diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ActionModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ActionModel.cs index a19e617175..bec68b4949 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ActionModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ActionModel.cs @@ -6,15 +6,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { - [DebuggerDisplay("Name={ActionName}({Methods()}), Type={Controller.ControllerType.Name}," + - " Route: {AttributeRouteModel?.Template}, Filters: {Filters.Count}")] + [DebuggerDisplay("{Controller.ControllerType.Name}.{ActionMethod.Name}")] public class ActionModel : ICommonModel, IFilterModel, IApiExplorerModel { public ActionModel( @@ -35,12 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels ApiExplorer = new ApiExplorerModel(); Attributes = new List(attributes); - ActionConstraints = new List(); Filters = new List(); - HttpMethods = new List(); Parameters = new List(); RouteConstraints = new List(); Properties = new Dictionary(); + Selectors = new List(); } public ActionModel(ActionModel other) @@ -57,25 +53,17 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Controller = other.Controller; // These are just metadata, safe to create new collections - ActionConstraints = new List(other.ActionConstraints); Attributes = new List(other.Attributes); Filters = new List(other.Filters); - HttpMethods = new List(other.HttpMethods); Properties = new Dictionary(other.Properties); // Make a deep copy of other 'model' types. ApiExplorer = new ApiExplorerModel(other.ApiExplorer); Parameters = new List(other.Parameters.Select(p => new ParameterModel(p))); RouteConstraints = new List(other.RouteConstraints); - - if (other.AttributeRouteModel != null) - { - AttributeRouteModel = new AttributeRouteModel(other.AttributeRouteModel); - } + Selectors = new List(other.Selectors.Select(s => new SelectorModel(s))); } - public IList ActionConstraints { get; private set; } - public MethodInfo ActionMethod { get; } public string ActionName { get; set; } @@ -86,25 +74,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// /// allows configuration of settings for ApiExplorer /// which apply to the action. - /// + /// /// Settings applied by override settings from /// and . /// public ApiExplorerModel ApiExplorer { get; set; } - public AttributeRouteModel AttributeRouteModel { get; set; } - public IReadOnlyList Attributes { get; } public ControllerModel Controller { get; set; } - public IList Filters { get; private set; } + public IList Filters { get; } - public IList HttpMethods { get; private set; } + public IList Parameters { get; } - public IList Parameters { get; private set; } - - public IList RouteConstraints { get; private set; } + public IList RouteConstraints { get; } /// /// Gets a set of properties associated with the action. @@ -120,14 +104,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels string ICommonModel.Name => ActionName; - private string Methods() - { - if (HttpMethods.Count == 0) - { - return "All"; - } - - return string.Join(", ", HttpMethods); - } + public IList Selectors { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ControllerModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ControllerModel.cs index ddc12f701b..9c58a541ef 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ControllerModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ControllerModel.cs @@ -6,14 +6,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { - [DebuggerDisplay("Name={ControllerName}, Type={ControllerType.Name}," + - " Routes: {AttributeRoutes.Count}, Filters: {Filters.Count}")] + [DebuggerDisplay("Name={ControllerName}, Type={ControllerType.Name}")] public class ControllerModel : ICommonModel, IFilterModel, IApiExplorerModel { public ControllerModel( @@ -35,12 +33,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Actions = new List(); ApiExplorer = new ApiExplorerModel(); Attributes = new List(attributes); - AttributeRoutes = new List(); - ActionConstraints = new List(); - Filters = new List(); - RouteConstraints = new List(); - Properties = new Dictionary(); ControllerProperties = new List(); + Filters = new List(); + Properties = new Dictionary(); + RouteConstraints = new List(); + Selectors = new List(); } public ControllerModel(ControllerModel other) @@ -57,7 +54,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Application = other.Application; // These are just metadata, safe to create new collections - ActionConstraints = new List(other.ActionConstraints); Attributes = new List(other.Attributes); Filters = new List(other.Filters); RouteConstraints = new List(other.RouteConstraints); @@ -66,15 +62,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels // Make a deep copy of other 'model' types. Actions = new List(other.Actions.Select(a => new ActionModel(a))); ApiExplorer = new ApiExplorerModel(other.ApiExplorer); - AttributeRoutes = new List( - other.AttributeRoutes.Select(a => new AttributeRouteModel(a))); ControllerProperties = new List(other.ControllerProperties.Select(p => new PropertyModel(p))); + Selectors = new List(other.Selectors.Select(s => new SelectorModel(s))); } - public IList ActionConstraints { get; private set; } - - public IList Actions { get; private set; } + public IList Actions { get; } /// /// Gets or sets the for this controller. @@ -82,7 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// /// allows configuration of settings for ApiExplorer /// which apply to all actions in the controller unless overridden by . - /// + /// /// Settings applied by override settings from /// . /// @@ -90,8 +83,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public ApplicationModel Application { get; set; } - public IList AttributeRoutes { get; private set; } - public IReadOnlyList Attributes { get; } MemberInfo ICommonModel.MemberInfo => ControllerType; @@ -100,13 +91,13 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public string ControllerName { get; set; } - public TypeInfo ControllerType { get; private set; } + public TypeInfo ControllerType { get; } public IList ControllerProperties { get; } - public IList Filters { get; private set; } + public IList Filters { get; } - public IList RouteConstraints { get; private set; } + public IList RouteConstraints { get; } /// /// Gets a set of properties associated with the controller. @@ -117,5 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// in . /// public IDictionary Properties { get; } + + public IList Selectors { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs new file mode 100644 index 0000000000..e838e17c04 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/SelectorModel.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. 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 Microsoft.AspNetCore.Mvc.ActionConstraints; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + public class SelectorModel + { + public SelectorModel() + { + ActionConstraints = new List(); + } + + public SelectorModel(SelectorModel other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + ActionConstraints = new List(other.ActionConstraints); + + if (other.AttributeRouteModel != null) + { + AttributeRouteModel = new AttributeRouteModel(other.AttributeRouteModel); + } + } + + public AttributeRouteModel AttributeRouteModel { get; set; } + + public IList ActionConstraints { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs index f458e3a0f2..df6480d1b5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing.Tree; @@ -47,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal foreach (var controller in application.Controllers) { - // Only add properties which are explictly marked to bind. + // Only add properties which are explicitly marked to bind. // The attribute check is required for ModelBinder attribute. var controllerPropertyDescriptors = controller.ControllerProperties .Where(p => p.BindingInfo != null) @@ -128,7 +127,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // 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. + // constraint that prevents them from matching an incoming request. AddRemovalConstraints(actionDescriptor, removalConstraints); } else @@ -185,67 +184,87 @@ namespace Microsoft.AspNetCore.Mvc.Internal ControllerModel controller, ActionModel action) { + var controllerAttributeRoutes = controller.Selectors + .Where(sm => sm.AttributeRouteModel != null) + .Select(sm => sm.AttributeRouteModel) + .ToList(); + 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) + foreach (var actionSelectorModel in action.Selectors) { - // We're overriding the attribute routes on the controller, so filter out any metadata - // from controller level routes. - var actionDescriptor = CreateActionDescriptor( - action, - controllerAttributeRoute: null); + var actionAttributeRoute = actionSelectorModel.AttributeRouteModel; - 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) + // 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 (actionAttributeRoute != null && actionAttributeRoute.IsAbsoluteTemplate) { + // We're overriding the attribute routes on the controller, so filter out any metadata + // from controller level routes. var actionDescriptor = CreateActionDescriptor( action, - controllerAttributeRoute); + actionAttributeRoute, + controllerAttributeRoute: null); actionDescriptors.Add(actionDescriptor); + AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters); + // 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); + IList controllerConstraints = null; + if (controller.Selectors.Count > 0) + { + controllerConstraints = controller.Selectors[0].ActionConstraints + .Where(constraint => !(constraint is IRouteTemplateProvider)).ToList(); + } - var controllerConstraints = controller.ActionConstraints - .Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider)); - AddActionConstraints(actionDescriptor, action, controllerConstraints); + AddActionConstraints(actionDescriptor, action, actionSelectorModel, controllerConstraints); } - } - else - { - // No attribute routes on the controller - var actionDescriptor = CreateActionDescriptor( - action, - controllerAttributeRoute: null); - actionDescriptors.Add(actionDescriptor); + else if (controllerAttributeRoutes.Count > 0) + { + // We're using the attribute routes from the controller + foreach (var controllerSelectorModel in controller.Selectors) + { + var controllerAttributeRoute = controllerSelectorModel.AttributeRouteModel; - // 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); + var actionDescriptor = CreateActionDescriptor( + action, + actionAttributeRoute, + controllerAttributeRoute); + + actionDescriptors.Add(actionDescriptor); + + AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters); + + // If we're using an attribute route on the controller, then filter out any additional + // metadata from the 'other' attribute routes. + var controllerConstraints = controllerSelectorModel.ActionConstraints + .Where(c => c == controllerAttributeRoute?.Attribute || !(c is IRouteTemplateProvider)); + AddActionConstraints(actionDescriptor, action, actionSelectorModel, controllerConstraints); + } + } + else + { + // No attribute routes on the controller + var actionDescriptor = CreateActionDescriptor( + action, + actionAttributeRoute, + controllerAttributeRoute: null); + actionDescriptors.Add(actionDescriptor); + + IList controllerConstraints = null; + if (controller.Selectors.Count > 0) + { + controllerConstraints = controller.Selectors[0].ActionConstraints; + } + + // If there's no attribute route on the controller, then we use all of the filters/constraints + // on the controller regardless. + AddActionFilters(actionDescriptor, action.Filters, controller.Filters, application.Filters); + AddActionConstraints(actionDescriptor, action, actionSelectorModel, controllerConstraints); + } } return actionDescriptors; @@ -253,6 +272,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static ControllerActionDescriptor CreateActionDescriptor( ActionModel action, + AttributeRouteModel actionAttributeRoute, AttributeRouteModel controllerAttributeRoute) { var parameterDescriptors = new List(); @@ -262,17 +282,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal 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, + AttributeRouteInfo = CreateAttributeRouteInfo(actionAttributeRoute, controllerAttributeRoute) }; actionDescriptor.DisplayName = string.Format( @@ -316,15 +332,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal ControllerModel controller, ActionModel action) { - var isVisible = - action.ApiExplorer?.IsVisible ?? - controller.ApiExplorer?.IsVisible ?? + var isVisible = + action.ApiExplorer?.IsVisible ?? + controller.ApiExplorer?.IsVisible ?? application.ApiExplorer?.IsVisible ?? false; - var isVisibleSetOnActionOrController = - action.ApiExplorer?.IsVisible ?? - controller.ApiExplorer?.IsVisible ?? + var isVisibleSetOnActionOrController = + action.ApiExplorer?.IsVisible ?? + controller.ApiExplorer?.IsVisible ?? false; // ApiExplorer isn't supported on conventional-routed actions, but we still allow you to configure @@ -417,19 +433,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static void AddActionConstraints( ControllerActionDescriptor actionDescriptor, ActionModel action, + SelectorModel selectorModel, IEnumerable controllerConstraints) { var constraints = new List(); - var httpMethods = action.HttpMethods; - if (httpMethods != null && httpMethods.Count > 0) + if (selectorModel.ActionConstraints != null) { - constraints.Add(new HttpMethodActionConstraint(httpMethods)); - } - - if (action.ActionConstraints != null) - { - constraints.AddRange(action.ActionConstraints); + constraints.AddRange(selectorModel.ActionConstraints); } if (controllerConstraints != null) @@ -449,7 +460,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal ControllerModel controller, ActionModel action) { - // Apply all the constraints defined on the action, then controller (for example, [Area]) + // Apply all the constraints defined on the action, then controller (for example, [Area]) // to the actions. Also keep track of all the constraints that require preventing actions // without the constraint to match. For example, actions without an [Area] attribute on their // controller should not match when a value has been given for area when matching a url or @@ -557,7 +568,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal 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. + // and precedence of attribute routes allow this kind of behavior. if (constraint.KeyHandling == RouteKeyHandling.RequireKey || constraint.KeyHandling == RouteKeyHandling.DenyKey) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs index 9099575073..708a355011 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultApplicationModelProvider.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Internal; @@ -50,46 +49,44 @@ namespace Microsoft.AspNetCore.Mvc.Internal foreach (var controllerType in context.ControllerTypes) { - var controllerModels = BuildControllerModels(controllerType); - if (controllerModels != null) + var controllerModel = CreateControllerModel(controllerType); + if (controllerModel == null) { - foreach (var controllerModel in controllerModels) + continue; + } + + context.Result.Controllers.Add(controllerModel); + controllerModel.Application = context.Result; + + foreach (var propertyHelper in PropertyHelper.GetProperties(controllerType.AsType())) + { + var propertyInfo = propertyHelper.Property; + var propertyModel = CreatePropertyModel(propertyInfo); + if (propertyModel != null) { - context.Result.Controllers.Add(controllerModel); - controllerModel.Application = context.Result; + propertyModel.Controller = controllerModel; + controllerModel.ControllerProperties.Add(propertyModel); + } + } - foreach (var propertyHelper in PropertyHelper.GetProperties(controllerType.AsType())) + foreach (var methodInfo in controllerType.AsType().GetMethods()) + { + var actionModel = CreateActionModel(controllerType, methodInfo); + if (actionModel == null) + { + continue; + } + + actionModel.Controller = controllerModel; + controllerModel.Actions.Add(actionModel); + + foreach (var parameterInfo in actionModel.ActionMethod.GetParameters()) + { + var parameterModel = CreateParameterModel(parameterInfo); + if (parameterModel != null) { - var propertyInfo = propertyHelper.Property; - var propertyModel = CreatePropertyModel(propertyInfo); - if (propertyModel != null) - { - propertyModel.Controller = controllerModel; - controllerModel.ControllerProperties.Add(propertyModel); - } - } - - foreach (var methodInfo in controllerType.AsType().GetMethods()) - { - var actionModels = BuildActionModels(controllerType, methodInfo); - if (actionModels != null) - { - foreach (var actionModel in actionModels) - { - actionModel.Controller = controllerModel; - controllerModel.Actions.Add(actionModel); - - foreach (var parameterInfo in actionModel.ActionMethod.GetParameters()) - { - var parameterModel = CreateParameterModel(parameterInfo); - if (parameterModel != null) - { - parameterModel.Action = actionModel; - actionModel.Parameters.Add(parameterModel); - } - } - } - } + parameterModel.Action = actionModel; + actionModel.Parameters.Add(parameterModel); } } } @@ -102,25 +99,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Intentionally empty. } - /// - /// Creates the instances for the given controller . - /// - /// The controller . - /// - /// A set of instances for the given controller or - /// null if the does not represent a controller. - /// - protected virtual IEnumerable BuildControllerModels(TypeInfo typeInfo) - { - if (typeInfo == null) - { - throw new ArgumentNullException(nameof(typeInfo)); - } - - var controllerModel = CreateControllerModel(typeInfo); - yield return controllerModel; - } - /// /// Creates a for the given . /// @@ -177,20 +155,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal filteredAttributes.Add(attribute); } } + filteredAttributes.AddRange(routeAttributes); attributes = filteredAttributes.ToArray(); var controllerModel = new ControllerModel(typeInfo, attributes); - AddRange( - controllerModel.AttributeRoutes, routeAttributes.Select(a => new AttributeRouteModel(a))); + + AddRange(controllerModel.Selectors, CreateSelectors(attributes)); controllerModel.ControllerName = typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? typeInfo.Name.Substring(0, typeInfo.Name.Length - "Controller".Length) : typeInfo.Name; - AddRange(controllerModel.ActionConstraints, attributes.OfType()); AddRange(controllerModel.Filters, attributes.OfType()); AddRange(controllerModel.RouteConstraints, attributes.OfType()); @@ -250,15 +228,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// - /// Creates the instances for the given action . + /// Creates the instance for the given action . /// /// The controller . /// The action . /// - /// A set of instances for the given action or + /// An instance for the given action or /// null if the does not represent an action. /// - protected virtual IEnumerable BuildActionModels( + protected virtual ActionModel CreateActionModel( TypeInfo typeInfo, MethodInfo methodInfo) { @@ -274,9 +252,44 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (!IsAction(typeInfo, methodInfo)) { - return Enumerable.Empty(); + return null; } + // 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(); + + var actionModel = new ActionModel(methodInfo, attributes); + + AddRange(actionModel.Filters, 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.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; + } + + var apiGroupName = attributes.OfType().FirstOrDefault(); + if (apiGroupName != null) + { + actionModel.ApiExplorer.GroupName = apiGroupName.GroupName; + } + + AddRange(actionModel.RouteConstraints, attributes.OfType()); + + //TODO: modify comment + // Now we need to determine the action selection info (cross-section of routes and constraints) + // // For attribute routes on a action, we want want to support 'overriding' routes on a // virtual method, but allow 'overriding'. So we need to walk up the hierarchy looking // for the first definition to define routes. @@ -309,10 +322,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal currentMethodInfo = nextMethodInfo; } - // 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(); - // This is fairly complicated so that we maintain referential equality between items in // ActionModel.Attributes and ActionModel.Attributes[*].Attribute. var applicableAttributes = new List(); @@ -327,134 +336,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal applicableAttributes.Add(attribute); } } + applicableAttributes.AddRange(routeAttributes); + AddRange(actionModel.Selectors, CreateSelectors(applicableAttributes)); - attributes = applicableAttributes.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 an action for each attribute that 'defines' a route, and a single action - // for all of the ones that don't (if any exist). - // - // If the attribute that 'defines' a route is NOT an IActionHttpMethodProvider, then we'll include with - // it, any IActionHttpMethodProvider that are 'silent' IRouteTemplateProviders. In this case the 'extra' - // action for silent route providers isn't needed. - // - // Ex: - // [HttpGet] - // [AcceptVerbs("POST", "PUT")] - // [HttpPost("Api/Things")] - // public void DoThing() - // - // This will generate 2 actions: - // 1. [HttpPost("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 routeProviders = new List(); - - var createActionForSilentRouteProviders = false; - foreach (var attribute in attributes) - { - var routeTemplateProvider = attribute as IRouteTemplateProvider; - if (routeTemplateProvider != null) - { - if (IsSilentRouteAttribute(routeTemplateProvider)) - { - createActionForSilentRouteProviders = true; - } - else - { - routeProviders.Add(attribute); - } - } - } - - foreach (var routeProvider in routeProviders) - { - // If we see an attribute like - // [Route(...)] - // - // Then we want to group any attributes like [HttpGet] with it. - // - // Basically... - // - // [HttpGet] - // [HttpPost("Products")] - // public void Foo() { } - // - // Is two actions. And... - // - // [HttpGet] - // [Route("Products")] - // public void Foo() { } - // - // Is one action. - if (!(routeProvider is IActionHttpMethodProvider)) - { - createActionForSilentRouteProviders = false; - } - } - - var actionModels = new List(); - if (routeProviders.Count == 0 && !createActionForSilentRouteProviders) - { - actionModels.Add(CreateActionModel(methodInfo, attributes)); - } - else - { - // Each of these routeProviders are the ones that actually have routing information on them - // something like [HttpGet] won't show up here, but [HttpGet("Products")] will. - foreach (var routeProvider in routeProviders) - { - var filteredAttributes = new List(); - foreach (var attribute in attributes) - { - if (attribute == routeProvider) - { - filteredAttributes.Add(attribute); - } - else if (routeProviders.Contains(attribute)) - { - // Exclude other route template providers - } - else if ( - routeProvider is IActionHttpMethodProvider && - attribute is IActionHttpMethodProvider) - { - // Exclude other http method providers if this route is an - // http method provider. - } - else - { - filteredAttributes.Add(attribute); - } - } - - actionModels.Add(CreateActionModel(methodInfo, filteredAttributes)); - } - - if (createActionForSilentRouteProviders) - { - var filteredAttributes = new List(); - foreach (var attribute in attributes) - { - if (!routeProviders.Contains(attribute)) - { - filteredAttributes.Add(attribute); - } - } - - actionModels.Add(CreateActionModel(methodInfo, filteredAttributes)); - } - } - - return actionModels; + return actionModel; } /// @@ -525,83 +411,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal return methodInfo.IsPublic; } - /// - /// 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( - MethodInfo methodInfo, - IReadOnlyList attributes) - { - if (methodInfo == null) - { - throw new ArgumentNullException(nameof(methodInfo)); - } - - if (attributes == null) - { - throw new ArgumentNullException(nameof(attributes)); - } - - var actionModel = new ActionModel(methodInfo, attributes); - - AddRange(actionModel.ActionConstraints, attributes.OfType()); - AddRange(actionModel.Filters, 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.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; - } - - var apiGroupName = attributes.OfType().FirstOrDefault(); - if (apiGroupName != null) - { - actionModel.ApiExplorer.GroupName = apiGroupName.GroupName; - } - - var httpMethods = attributes.OfType(); - AddRange(actionModel.HttpMethods, - httpMethods - .Where(a => a.HttpMethods != null) - .SelectMany(a => a.HttpMethods) - .Distinct()); - - AddRange(actionModel.RouteConstraints, attributes.OfType()); - - var routeTemplateProvider = - attributes - .OfType() - .SingleOrDefault(a => !IsSilentRouteAttribute(a)); - - if (routeTemplateProvider != null) - { - actionModel.AttributeRouteModel = new AttributeRouteModel(routeTemplateProvider); - } - - return actionModel; - } - /// /// Creates a for the given . /// @@ -627,6 +436,160 @@ namespace Microsoft.AspNetCore.Mvc.Internal return parameterModel; } + private IList CreateSelectors(IList attributes) + { + // Route attributes create multiple selector models, we want to split the set of + // attributes based on these so each selector 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 a selector for each attribute that 'defines' a route, and a single selector + // for all of the ones that don't (if any exist). + // + // If the attribute that 'defines' a route is NOT an IActionHttpMethodProvider, then we'll include with + // it, any IActionHttpMethodProvider that are 'silent' IRouteTemplateProviders. In this case the 'extra' + // action for silent route providers isn't needed. + // + // Ex: + // [HttpGet] + // [AcceptVerbs("POST", "PUT")] + // [HttpPost("Api/Things")] + // public void DoThing() + // + // This will generate 2 selectors: + // 1. [HttpPost("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 routeProviders = new List(); + + var createSelectorForSilentRouteProviders = false; + foreach (var attribute in attributes) + { + var routeTemplateProvider = attribute as IRouteTemplateProvider; + if (routeTemplateProvider != null) + { + if (IsSilentRouteAttribute(routeTemplateProvider)) + { + createSelectorForSilentRouteProviders = true; + } + else + { + routeProviders.Add(routeTemplateProvider); + } + } + } + + foreach (var routeProvider in routeProviders) + { + // If we see an attribute like + // [Route(...)] + // + // Then we want to group any attributes like [HttpGet] with it. + // + // Basically... + // + // [HttpGet] + // [HttpPost("Products")] + // public void Foo() { } + // + // Is two selectors. And... + // + // [HttpGet] + // [Route("Products")] + // public void Foo() { } + // + // Is one selector. + if (!(routeProvider is IActionHttpMethodProvider)) + { + createSelectorForSilentRouteProviders = false; + } + } + + var selectorModels = new List(); + if (routeProviders.Count == 0 && !createSelectorForSilentRouteProviders) + { + // Simple case, all attributes apply + selectorModels.Add(CreateSelectorModel(route: null, attributes: attributes)); + } + else + { + // Each of these routeProviders are the ones that actually have routing information on them + // something like [HttpGet] won't show up here, but [HttpGet("Products")] will. + foreach (var routeProvider in routeProviders) + { + var filteredAttributes = new List(); + foreach (var attribute in attributes) + { + if (attribute == routeProvider) + { + filteredAttributes.Add(attribute); + } + else if (routeProviders.Contains(attribute)) + { + // Exclude other route template providers + } + else if ( + routeProvider is IActionHttpMethodProvider && + attribute is IActionHttpMethodProvider) + { + // Exclude other http method providers if this route is an + // http method provider. + } + else + { + filteredAttributes.Add(attribute); + } + } + + selectorModels.Add(CreateSelectorModel(routeProvider, filteredAttributes)); + } + + if (createSelectorForSilentRouteProviders) + { + var filteredAttributes = new List(); + foreach (var attribute in attributes) + { + if (!routeProviders.Contains(attribute)) + { + filteredAttributes.Add(attribute); + } + } + + selectorModels.Add(CreateSelectorModel(route: null, attributes: filteredAttributes)); + } + } + + return selectorModels; + } + + private static SelectorModel CreateSelectorModel(IRouteTemplateProvider route, IList attributes) + { + var selectorModel = new SelectorModel(); + if (route != null) + { + selectorModel.AttributeRouteModel = new AttributeRouteModel(route); + } + + AddRange(selectorModel.ActionConstraints, attributes.OfType()); + + // Simple case, all HTTP method attributes apply + var httpMethods = attributes + .OfType() + .SelectMany(a => a.HttpMethods) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (httpMethods.Length > 0) + { + selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); + } + + return selectorModel; + } + private bool IsIDisposableMethod(MethodInfo methodInfo, TypeInfo typeInfo) { return diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsApplicationModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsApplicationModelConvention.cs index 3e04f370f7..e5acb57f82 100644 --- a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsApplicationModelConvention.cs +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiActionConventionsApplicationModelConvention.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim @@ -64,34 +64,55 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim private bool IsActionAttributeRouted(ActionModel action) { - if (action.Controller.AttributeRoutes.Count > 0) + foreach (var controllerSelectorModel in action.Controller.Selectors) { - return true; + if (controllerSelectorModel.AttributeRouteModel?.Template != null) + { + return true; + } } - return action.AttributeRouteModel?.Template != null; + foreach (var actionSelectorModel in action.Selectors) + { + if (actionSelectorModel.AttributeRouteModel?.Template != null) + { + return true; + } + } + + return false; } private void SetHttpMethodFromConvention(ActionModel action) { - if (action.HttpMethods.Count > 0) + foreach (var selector in action.Selectors) { - // If the HttpMethods are set from attributes, don't override it with the convention - return; + if (selector.ActionConstraints.OfType().Count() > 0) + { + // If the HttpMethods are set from attributes, don't override it with the convention + return; + } } - // The Method name is used to infer verb constraints. Changing the action name has not impact. + // The Method name is used to infer verb constraints. Changing the action name has no impact. foreach (var verb in SupportedHttpMethodConventions) { if (action.ActionMethod.Name.StartsWith(verb, StringComparison.OrdinalIgnoreCase)) { - action.HttpMethods.Add(verb); + foreach (var selector in action.Selectors) + { + selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { verb })); + } + return; } } // If no convention matches, then assume POST - action.HttpMethods.Add("POST"); + foreach (var actionSelectorModel in action.Selectors) + { + actionSelectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { "POST" })); + } } private class UnnamedActionRouteConstraint : IRouteConstraintProvider diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingApplicationModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingApplicationModelConvention.cs index 127abe2033..5a90c715ca 100644 --- a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingApplicationModelConvention.cs +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/Conventions/WebApiOverloadingApplicationModelConvention.cs @@ -18,7 +18,10 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim if (IsConventionApplicable(action.Controller)) { - action.ActionConstraints.Add(new OverloadActionConstraint()); + foreach (var actionSelectorModel in action.Selectors) + { + actionSelectorModel.ActionConstraints.Add(new OverloadActionConstraint()); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs index 923db68e2b..f29cda2d0c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public void CopyConstructor_DoesDeepCopyOfOtherModels() { // Arrange - var action = new ActionModel(typeof(TestController).GetMethod("Edit"), + var action = new ActionModel(typeof(TestController).GetMethod(nameof(TestController.Edit)), new List()); var parameter = new ParameterModel(action.ActionMethod.GetParameters()[0], @@ -26,7 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels action.Parameters.Add(parameter); var route = new AttributeRouteModel(new HttpGetAttribute("api/Products")); - action.AttributeRouteModel = route; + action.Selectors.Add(new SelectorModel() + { + AttributeRouteModel = route + }); var apiExplorer = action.ApiExplorer; apiExplorer.IsVisible = false; @@ -38,7 +41,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels // Assert Assert.NotSame(action, action2.Parameters[0]); Assert.NotSame(apiExplorer, action2.ApiExplorer); - Assert.NotSame(route, action2.AttributeRouteModel); + Assert.NotSame(action.Selectors, action2.Selectors); + Assert.NotNull(action2.Selectors); + Assert.Single(action2.Selectors); + Assert.NotSame(route, action2.Selectors[0].AttributeRouteModel); } [Fact] @@ -53,13 +59,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels new MyFilterAttribute(), }); - action.ActionConstraints.Add(new HttpMethodActionConstraint(new string[] { "GET" })); + var selectorModel = new SelectorModel(); + selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new string[] { "GET" })); + action.Selectors.Add(selectorModel); action.ActionName = "Edit"; action.Controller = new ControllerModel(typeof(TestController).GetTypeInfo(), new List()); action.Filters.Add(new MyFilterAttribute()); - action.HttpMethods.Add("GET"); action.RouteConstraints.Add(new MyRouteConstraintAttribute()); action.Properties.Add(new KeyValuePair("test key", "test value")); @@ -71,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { // Reflection is used to make sure the test fails when a new property is added. if (property.Name.Equals("ApiExplorer") || - property.Name.Equals("AttributeRouteModel") || + property.Name.Equals("Selectors") || property.Name.Equals("Parameters")) { // This test excludes other ApplicationModel objects on purpose because we deep copy them. diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs index 1b640f3c33..497004adfe 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels action.Controller = controller; var route = new AttributeRouteModel(new HttpGetAttribute("api/Products")); - controller.AttributeRoutes.Add(route); + controller.Selectors.Add(new SelectorModel() { AttributeRouteModel = route }); var apiExplorer = controller.ApiExplorer; controller.ApiExplorer.GroupName = "group"; @@ -39,10 +39,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels // Assert Assert.NotSame(action, controller2.Actions[0]); - Assert.NotSame(route, controller2.AttributeRoutes[0]); + Assert.NotNull(controller2.Selectors); + Assert.Single(controller2.Selectors); + Assert.NotSame(route, controller2.Selectors[0].AttributeRouteModel); Assert.NotSame(apiExplorer, controller2.ApiExplorer); - Assert.NotSame(controller.ActionConstraints, controller2.ActionConstraints); + Assert.NotSame(controller.Selectors[0].ActionConstraints, controller2.Selectors[0].ActionConstraints); Assert.NotSame(controller.Actions, controller2.Actions); Assert.NotSame(controller.Attributes, controller2.Attributes); Assert.NotSame(controller.Filters, controller2.Filters); @@ -61,7 +63,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels new MyFilterAttribute(), }); - controller.ActionConstraints.Add(new HttpMethodActionConstraint(new string[] { "GET" })); + var selectorModel = new SelectorModel(); + selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(new string[] { "GET" })); + controller.Selectors.Add(selectorModel); controller.Application = new ApplicationModel(); controller.ControllerName = "cool"; controller.Filters.Add(new MyFilterAttribute()); @@ -77,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels foreach (var property in typeof(ControllerModel).GetProperties()) { if (property.Name.Equals("Actions") || - property.Name.Equals("AttributeRoutes") || + property.Name.Equals("Selectors") || property.Name.Equals("ApiExplorer") || property.Name.Equals("ControllerProperties")) { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AuthorizationApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AuthorizationApplicationModelProviderTest.cs index aabcc5f0a5..c598ce7069 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AuthorizationApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/AuthorizationApplicationModelProviderTest.cs @@ -51,7 +51,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal var controller = Assert.Single(context.Result.Controllers); var action = Assert.Single(controller.Actions); Assert.Equal("Authorize", action.ActionName); - Assert.Null(action.AttributeRouteModel); + + var attributeRoutes = action.Selectors.Where(sm => sm.AttributeRouteModel != null); + Assert.Empty(attributeRoutes); var authorizeFilters = action.Filters.OfType(); Assert.Single(authorizeFilters); Assert.Equal(3, authorizeFilters.First().Policy.Requirements.Count); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorBuilderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorBuilderTest.cs index 8638087f8e..17e5325d91 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorBuilderTest.cs @@ -40,8 +40,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal controller.Application = applicationModel; applicationModel.Controllers.Add(controller); - var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var methodInfo = typeof(TestController).GetMethod(nameof(TestController.SomeAction)); var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Selectors.Add(new SelectorModel()); actionModel.Controller = controller; controller.Actions.Add(actionModel); @@ -71,8 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal controller.Application = applicationModel; applicationModel.Controllers.Add(controller); - var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var methodInfo = typeof(TestController).GetMethod(nameof(TestController.SomeAction)); var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Selectors.Add(new SelectorModel()); actionModel.Controller = controller; controller.Actions.Add(actionModel); @@ -96,8 +98,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal controller.Properties["test"] = "controller"; applicationModel.Controllers.Add(controller); - var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var methodInfo = typeof(TestController).GetMethod(nameof(TestController.SomeAction)); var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Selectors.Add(new SelectorModel()); actionModel.Controller = controller; controller.Actions.Add(actionModel); @@ -121,8 +124,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal controller.Properties["test"] = "controller"; applicationModel.Controllers.Add(controller); - var methodInfo = typeof(TestController).GetMethod("SomeAction"); + var methodInfo = typeof(TestController).GetMethod(nameof(TestController.SomeAction)); var actionModel = new ActionModel(methodInfo, new List() { }); + actionModel.Selectors.Add(new SelectorModel()); actionModel.Controller = controller; actionModel.Properties["test"] = "action"; controller.Actions.Add(actionModel); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs index c4f7ad4863..de792743f9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionDescriptorProviderTests.cs @@ -416,13 +416,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal var conventional = Assert.Single(model.Controllers, c => c.ControllerName == "ConventionallyRouted"); - Assert.Empty(conventional.AttributeRoutes); + Assert.Empty(conventional.Selectors.Where(sm => sm.AttributeRouteModel != null)); Assert.Single(conventional.Actions); var attributeRouted = Assert.Single(model.Controllers, c => c.ControllerName == "AttributeRouted"); Assert.Single(attributeRouted.Actions); - Assert.Single(attributeRouted.AttributeRoutes); + Assert.Single(attributeRouted.Selectors.Where(sm => sm.AttributeRouteModel != null)); var empty = Assert.Single(model.Controllers, c => c.ControllerName == "Empty"); @@ -449,10 +449,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Equal(2, controller.Actions.Count); var getPerson = Assert.Single(controller.Actions, a => a.ActionName == "GetPerson"); - Assert.Empty(getPerson.HttpMethods); + Assert.Empty(getPerson.Selectors[0].ActionConstraints.OfType()); var showPeople = Assert.Single(controller.Actions, a => a.ActionName == "ShowPeople"); - Assert.Empty(showPeople.HttpMethods); + Assert.Empty(showPeople.Selectors[0].ActionConstraints.OfType()); } [Fact] @@ -486,11 +486,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var controller = Assert.Single(model.Controllers); - var attributeRouteModel = Assert.Single(controller.AttributeRoutes); - Assert.Equal("api/Token/[key]/[controller]", attributeRouteModel.Template); + var selectorModel = Assert.Single(controller.Selectors.Where(sm => sm.AttributeRouteModel != null)); + Assert.Equal("api/Token/[key]/[controller]", selectorModel.AttributeRouteModel.Template); var action = Assert.Single(controller.Actions); - Assert.Equal("stub/[action]", action.AttributeRouteModel.Template); + var actionSelectorModel = Assert.Single(action.Selectors.Where(sm => sm.AttributeRouteModel != null)); + Assert.Equal("stub/[action]", actionSelectorModel.AttributeRouteModel.Template); } [Fact] @@ -567,7 +568,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var descriptors = provider.GetDescriptors(); // Assert - var actions = descriptors.Where(d => d.Name == "AcceptVerbs"); + var actions = descriptors.Where(d => d.Name == nameof(MultiRouteAttributesController.AcceptVerbs)); Assert.Equal(2, actions.Count()); foreach (var action in actions) @@ -1300,28 +1301,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal var model = provider.BuildModel(); // Assert - var actions = Assert.Single(model.Controllers).Actions; - Assert.Equal(2, actions.Count()); + var controllerModel = Assert.Single(model.Controllers); + var actionModel = Assert.Single(controllerModel.Actions); + Assert.Equal(3, actionModel.Attributes.Count); + Assert.Equal(2, actionModel.Attributes.OfType().Count()); + Assert.Single(actionModel.Attributes.OfType()); + Assert.Equal(2, actionModel.Selectors.Count); - var action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "R1"); + var selectorModel = Assert.Single( + actionModel.Selectors.Where(sm => sm.AttributeRouteModel?.Template == "R1")); - Assert.Equal(2, action.Attributes.Count); - Assert.Single(action.Attributes, a => a is RouteAndConstraintAttribute); - Assert.Single(action.Attributes, a => a is ConstraintAttribute); + Assert.Equal(2, selectorModel.ActionConstraints.Count); + Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Single(selectorModel.ActionConstraints.OfType()); - Assert.Equal(2, action.ActionConstraints.Count); - Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute); - Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute); + selectorModel = Assert.Single( + actionModel.Selectors.Where(sm => sm.AttributeRouteModel?.Template == "R2")); - action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "R2"); - - Assert.Equal(2, action.Attributes.Count); - Assert.Single(action.Attributes, a => a is RouteAndConstraintAttribute); - Assert.Single(action.Attributes, a => a is ConstraintAttribute); - - Assert.Equal(2, action.ActionConstraints.Count); - Assert.Single(action.ActionConstraints, a => a is RouteAndConstraintAttribute); - Assert.Single(action.ActionConstraints, a => a is ConstraintAttribute); + Assert.Equal(2, selectorModel.ActionConstraints.Count); + Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Single(selectorModel.ActionConstraints.OfType()); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs index 3978cf66c6..5931328b24 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs @@ -8,7 +8,6 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Options; @@ -112,13 +111,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal var model = builder.CreateControllerModel(typeInfo); // Assert - Assert.Equal(2, model.AttributeRoutes.Count); + var attributeRoutes = GetAttributeRoutes(model.Selectors); + Assert.Equal(2, attributeRoutes.Count); Assert.Equal(2, model.Attributes.Count); - var route = Assert.Single(model.AttributeRoutes, r => r.Template == "A"); + var route = Assert.Single(attributeRoutes, r => r.Template == "A"); Assert.Contains(route.Attribute, model.Attributes); - route = Assert.Single(model.AttributeRoutes, r => r.Template == "B"); + route = Assert.Single(attributeRoutes, r => r.Template == "B"); Assert.Contains(route.Attribute, model.Attributes); } @@ -133,13 +133,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal var model = builder.CreateControllerModel(typeInfo); // Assert - Assert.Equal(2, model.AttributeRoutes.Count); + var attributeRoutes = GetAttributeRoutes(model.Selectors); + Assert.Equal(2, attributeRoutes.Count); Assert.Equal(2, model.Attributes.Count); - var route = Assert.Single(model.AttributeRoutes, r => r.Template == "C"); + var route = Assert.Single(attributeRoutes, r => r.Template == "C"); Assert.Contains(route.Attribute, model.Attributes); - route = Assert.Single(model.AttributeRoutes, r => r.Template == "D"); + route = Assert.Single(attributeRoutes, r => r.Template == "D"); Assert.Contains(route.Attribute, model.Attributes); } @@ -413,7 +414,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } [Fact] - public void BuildActionModels_ConventionallyRoutedAction_WithoutHttpConstraints() + public void CreateActionModel_ConventionallyRoutedAction_WithoutHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); @@ -421,18 +422,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(ConventionallyRoutedController.Edit); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); - Assert.Equal("Edit", action.ActionName); - Assert.Empty(action.HttpMethods); - Assert.Null(action.AttributeRouteModel); + Assert.NotNull(action); + Assert.Equal(actionName, action.ActionName); Assert.Empty(action.Attributes); + Assert.Single(action.Selectors); + Assert.Empty(action.Selectors[0].ActionConstraints.OfType()); + Assert.Empty(GetAttributeRoutes(action.Selectors)); } [Fact] - public void BuildActionModels_ConventionallyRoutedAction_WithHttpConstraints() + public void CreateActionModel_ConventionallyRoutedAction_WithHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); @@ -440,20 +442,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(ConventionallyRoutedController.Update); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); - Assert.Contains("PUT", action.HttpMethods); - Assert.Contains("PATCH", action.HttpMethods); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); + Assert.Contains("PUT", methodConstraint.HttpMethods); + Assert.Contains("PATCH", methodConstraint.HttpMethods); - Assert.Equal("Update", action.ActionName); - Assert.Null(action.AttributeRouteModel); + Assert.Equal(actionName, action.ActionName); + Assert.Empty(GetAttributeRoutes(action.Selectors)); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] - public void BuildActionModels_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider() + public void CreateActionModel_ConventionallyRoutedActionWithHttpConstraints_AndInvalidRouteTemplateProvider() { // Arrange var builder = new TestApplicationModelProvider(); @@ -461,21 +466,24 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(ConventionallyRoutedController.Delete); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); - Assert.Contains("DELETE", action.HttpMethods); - Assert.Contains("HEAD", action.HttpMethods); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); + Assert.Contains("DELETE", methodConstraint.HttpMethods); + Assert.Contains("HEAD", methodConstraint.HttpMethods); - Assert.Equal("Delete", action.ActionName); - Assert.Null(action.AttributeRouteModel); + Assert.Equal(actionName, action.ActionName); + Assert.Empty(GetAttributeRoutes(action.Selectors)); Assert.Single(action.Attributes.OfType()); Assert.Single(action.Attributes.OfType()); } [Fact] - public void BuildActionModels_ConventionallyRoutedAction_WithMultipleHttpConstraints() + public void CreateActionModel_ConventionallyRoutedAction_WithMultipleHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); @@ -483,19 +491,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(ConventionallyRoutedController.Details); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); - Assert.Contains("GET", action.HttpMethods); - Assert.Contains("POST", action.HttpMethods); - Assert.Contains("HEAD", action.HttpMethods); - Assert.Equal("Details", action.ActionName); - Assert.Null(action.AttributeRouteModel); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); + Assert.Contains("GET", methodConstraint.HttpMethods); + Assert.Contains("POST", methodConstraint.HttpMethods); + Assert.Contains("HEAD", methodConstraint.HttpMethods); + Assert.Equal(actionName, action.ActionName); + Assert.Empty(GetAttributeRoutes(action.Selectors)); } [Fact] - public void BuildActionModels_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints() + public void CreateActionModel_ConventionallyRoutedAction_WithMultipleOverlappingHttpConstraints() { // Arrange var builder = new TestApplicationModelProvider(); @@ -503,19 +514,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(ConventionallyRoutedController.List); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().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.Null(action.AttributeRouteModel); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); + Assert.Contains("GET", methodConstraint.HttpMethods); + Assert.Contains("PUT", methodConstraint.HttpMethods); + Assert.Contains("POST", methodConstraint.HttpMethods); + Assert.Equal(actionName, action.ActionName); + Assert.Empty(GetAttributeRoutes(action.Selectors)); } [Fact] - public void BuildActionModels_AttributeRouteOnAction() + public void CreateActionModel_AttributeRouteOnAction() { // Arrange var builder = new TestApplicationModelProvider(); @@ -523,24 +537,27 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(NoRouteAttributeOnControllerController.Edit); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); - Assert.Equal("Edit", action.ActionName); + Assert.Equal(actionName, action.ActionName); - var httpMethod = Assert.Single(action.HttpMethods); + var httpMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("HEAD", httpMethod); - Assert.NotNull(action.AttributeRouteModel); - Assert.Equal("Change", action.AttributeRouteModel.Template); + var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); + Assert.Equal("Change", attributeRoute.Template); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] - public void BuildActionModels_AttributeRouteOnAction_RouteAttribute() + public void CreateActionModel_AttributeRouteOnAction_RouteAttribute() { // Arrange var builder = new TestApplicationModelProvider(); @@ -548,23 +565,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(NoRouteAttributeOnControllerController.Update); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); + Assert.NotNull(action); + Assert.Single(action.Selectors); + Assert.Empty(action.Selectors[0].ActionConstraints); - Assert.Equal("Update", action.ActionName); + Assert.Equal(actionName, action.ActionName); - Assert.Empty(action.HttpMethods); - - Assert.NotNull(action.AttributeRouteModel); - Assert.Equal("Update", action.AttributeRouteModel.Template); + var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); + Assert.Equal("Update", attributeRoute.Template); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] - public void BuildActionModels_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate() + public void CreateActionModel_AttributeRouteOnAction_AcceptVerbsAttributeWithTemplate() { // Arrange var builder = new TestApplicationModelProvider(); @@ -572,23 +589,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(NoRouteAttributeOnControllerController.List); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); - Assert.Equal("List", action.ActionName); + Assert.Equal(actionName, action.ActionName); - Assert.Equal(new[] { "GET", "HEAD" }, action.HttpMethods.OrderBy(m => m, StringComparer.Ordinal)); + Assert.Equal( + new[] { "GET", "HEAD" }, + methodConstraint.HttpMethods.OrderBy(m => m, StringComparer.Ordinal)); - Assert.NotNull(action.AttributeRouteModel); - Assert.Equal("ListAll", action.AttributeRouteModel.Template); + var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); + Assert.Equal("ListAll", attributeRoute.Template); Assert.IsType(Assert.Single(action.Attributes)); } [Fact] - public void BuildActionModels_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate() + public void CreateActionModel_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate() { // Arrange var builder = new TestApplicationModelProvider(); @@ -596,30 +618,35 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(NoRouteAttributeOnControllerController.Index); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - Assert.Equal(2, actions.Count()); + Assert.NotNull(action); + Assert.Equal(actionName, action.ActionName); + Assert.NotNull(action.Attributes); + Assert.Equal(2, action.Attributes.Count); + Assert.Single(action.Attributes.OfType()); + Assert.Single(action.Attributes.OfType()); + Assert.Equal(2, action.Selectors.Count); - foreach (var action in actions) + foreach (var actionSelectorModel in action.Selectors) { - Assert.Equal("Index", action.ActionName); - Assert.NotNull(action.AttributeRouteModel); + Assert.NotNull(actionSelectorModel.AttributeRouteModel); } - var list = Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("List")); - var listMethod = Assert.Single(list.HttpMethods); + var selectorModel = Assert.Single(action.Selectors, ai => ai.AttributeRouteModel?.Template == "List"); + var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + var listMethod = Assert.Single(methodConstraint.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); + var all = Assert.Single(action.Selectors, ai => ai.AttributeRouteModel?.Template == "All"); + methodConstraint = Assert.Single(all.ActionConstraints.OfType()); + var allMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("GET", allMethod); - Assert.IsType(Assert.Single(all.Attributes)); } [Fact] - public void BuildActionModels_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController() + public void CreateActionModel_NoRouteOnController_AllowsConventionallyRoutedActions_OnTheSameController() { // Arrange var builder = new TestApplicationModelProvider(); @@ -627,77 +654,74 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(NoRouteAttributeOnControllerController.Remove); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); - - Assert.Equal("Remove", action.ActionName); - - Assert.Empty(action.HttpMethods); - - Assert.Null(action.AttributeRouteModel); + Assert.NotNull(action); + Assert.Equal(actionName, action.ActionName); Assert.Empty(action.Attributes); + Assert.Single(action.Selectors); + Assert.Empty(action.Selectors[0].ActionConstraints); + Assert.Null(action.Selectors[0].AttributeRouteModel); } [Theory] [InlineData(typeof(SingleRouteAttributeController))] [InlineData(typeof(MultipleRouteAttributeController))] - public void BuildActionModels_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller) + public void CreateActionModel_RouteAttributeOnController_CreatesAttributeRoute_ForNonAttributedActions(Type controller) { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = controller.GetTypeInfo(); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod("Delete")); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod("Delete")); // Assert - var action = Assert.Single(actions); + Assert.NotNull(action); Assert.Equal("Delete", action.ActionName); - Assert.Empty(action.HttpMethods); - - Assert.Null(action.AttributeRouteModel); - + Assert.Single(action.Selectors); + Assert.Empty(action.Selectors[0].ActionConstraints); + Assert.Empty(GetAttributeRoutes(action.Selectors)); Assert.Empty(action.Attributes); } [Theory] [InlineData(typeof(SingleRouteAttributeController))] [InlineData(typeof(MultipleRouteAttributeController))] - public void BuildActionModels_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller) + public void CreateActionModel_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller) { // Arrange var builder = new TestApplicationModelProvider(); var typeInfo = controller.GetTypeInfo(); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod("Index")); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod("Index")); // Assert - Assert.Equal(2, actions.Count()); + Assert.NotNull(action.Attributes); + Assert.Equal(2, action.Attributes.Count); + Assert.Equal(2, action.Selectors.Count); + Assert.Equal("Index", action.ActionName); - foreach (var action in actions) + foreach (var selectorModel in action.Selectors) { - Assert.Equal("Index", action.ActionName); - - var httpMethod = Assert.Single(action.HttpMethods); + var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + var httpMethod = Assert.Single(methodConstraint.HttpMethods); Assert.Equal("GET", httpMethod); - Assert.NotNull(action.AttributeRouteModel.Template); - - Assert.IsType(Assert.Single(action.Attributes)); + Assert.NotNull(selectorModel.AttributeRouteModel.Template); } - Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("List")); - Assert.Single(actions, ai => ai.AttributeRouteModel.Template.Equals("All")); + Assert.Single(action.Selectors, ai => ai.AttributeRouteModel.Template.Equals("List")); + Assert.Single(action.Selectors, ai => ai.AttributeRouteModel.Template.Equals("All")); } [Fact] - public void BuildActionModels_MixedHttpVerbsAndRoutes_EmptyVerbWithRoute() + public void CreateActionModel_MixedHttpVerbsAndRoutes_EmptyVerbWithRoute() { // Arrange var builder = new TestApplicationModelProvider(); @@ -705,16 +729,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.VerbAndRoute); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - var action = Assert.Single(actions); - Assert.Equal(new string[] { "GET" }, action.HttpMethods); - Assert.Equal("Products", action.AttributeRouteModel.Template); + Assert.NotNull(action); + Assert.Single(action.Selectors); + var methodConstraint = Assert.Single( + action.Selectors[0].ActionConstraints.OfType()); + Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); + var attributeRoute = Assert.Single(GetAttributeRoutes(action.Selectors)); + Assert.Equal("Products", attributeRoute.Template); } [Fact] - public void BuildActionModels_MixedHttpVerbsAndRoutes_MultipleEmptyVerbsWithMultipleRoutes() + public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyVerbsWithMultipleRoutes() { // Arrange var builder = new TestApplicationModelProvider(); @@ -722,21 +750,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.MultipleVerbsAndRoutes); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var actions = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - Assert.Equal(2, actions.Count()); + Assert.Equal(2, actions.Selectors.Count); // OrderBy is used because the order of the results may very depending on the platform / client. - var action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "Products"); - Assert.Equal(new[] { "GET", "POST" }, action.HttpMethods.OrderBy(key => key, StringComparer.Ordinal)); + var selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel.Template == "Products"); + var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new[] { "GET", "POST" }, methodConstraint.HttpMethods.OrderBy(key => key, StringComparer.Ordinal)); - action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "v2/Products"); - Assert.Equal(new[] { "GET", "POST" }, action.HttpMethods.OrderBy(key => key, StringComparer.Ordinal)); + selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel.Template == "v2/Products"); + methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new[] { "GET", "POST" }, methodConstraint.HttpMethods.OrderBy(key => key, StringComparer.Ordinal)); } [Fact] - public void BuildActionModels_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbsWithMultipleRoutes() + public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbsWithMultipleRoutes() { // Arrange var builder = new TestApplicationModelProvider(); @@ -744,23 +774,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.MultipleVerbsWithAnyWithoutTemplateAndRoutes); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - Assert.Equal(3, actions.Count()); + Assert.Equal(3, action.Selectors.Count); - var action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "Products"); - Assert.Equal(new string[] { "GET" }, action.HttpMethods); + var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "Products"); + var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); - action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "v2/Products"); - Assert.Equal(new string[] { "GET" }, action.HttpMethods); + selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "v2/Products"); + methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); - action = Assert.Single(actions, a => a.AttributeRouteModel.Template == "Products/Buy"); - Assert.Equal(new string[] { "POST" }, action.HttpMethods); + selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel.Template == "Products/Buy"); + methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new string[] { "POST" }, methodConstraint.HttpMethods); } [Fact] - public void BuildActionModels_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbs() + public void CreateActionModel_MixedHttpVerbsAndRoutes_MultipleEmptyAndNonEmptyVerbs() { // Arrange var builder = new TestApplicationModelProvider(); @@ -768,20 +801,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(MixedHttpVerbsAndRouteAttributeController.Invalid); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - Assert.Equal(2, actions.Count()); + Assert.NotNull(action); + Assert.Equal(2, action.Selectors.Count); - var action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "Products"); - Assert.Equal(new string[] { "POST" }, action.HttpMethods); + var selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == "Products"); + var methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new string[] { "POST" }, methodConstraint.HttpMethods); - action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == null); - Assert.Equal(new string[] { "GET" }, action.HttpMethods); + selectorModel = Assert.Single(action.Selectors, s => s.AttributeRouteModel?.Template == null); + methodConstraint = Assert.Single(selectorModel.ActionConstraints.OfType()); + Assert.Equal(new string[] { "GET" }, methodConstraint.HttpMethods); } [Fact] - public void BuildActionModels_InheritedAttributeRoutes() + public void CreateActionModel_InheritedAttributeRoutes() { // Arrange var builder = new TestApplicationModelProvider(); @@ -789,22 +825,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(DerivedClassInheritsAttributeRoutesController.Edit); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var actions = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - Assert.Equal(2, actions.Count()); + Assert.Equal(2, actions.Attributes.Count); + Assert.Equal(2, actions.Selectors.Count); - var action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "A"); - Assert.Equal(1, action.Attributes.Count); - Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes); + var selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel?.Template == "A"); + Assert.Contains(selectorModel.AttributeRouteModel.Attribute, actions.Attributes); - action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "B"); - Assert.Equal(1, action.Attributes.Count); - Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes); + selectorModel = Assert.Single(actions.Selectors, a => a.AttributeRouteModel?.Template == "B"); + Assert.Contains(selectorModel.AttributeRouteModel.Attribute, actions.Attributes); } [Fact] - public void BuildActionModels_InheritedAttributeRoutesOverridden() + public void CreateActionModel_InheritedAttributeRoutesOverridden() { // Arrange var builder = new TestApplicationModelProvider(); @@ -812,18 +847,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal var actionName = nameof(DerivedClassOverridesAttributeRoutesController.Edit); // Act - var actions = builder.BuildActionModels(typeInfo, typeInfo.AsType().GetMethod(actionName)); + var action = builder.CreateActionModel(typeInfo, typeInfo.AsType().GetMethod(actionName)); // Assert - Assert.Equal(2, actions.Count()); + Assert.Equal(4, action.Attributes.Count); + Assert.Equal(2, action.Selectors.Count); - var action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "C"); - Assert.Equal(1, action.Attributes.Count); - Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes); + var selectorModel = Assert.Single(action.Selectors, a => a.AttributeRouteModel?.Template == "C"); + Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes); - action = Assert.Single(actions, a => a.AttributeRouteModel?.Template == "D"); - Assert.Equal(1, action.Attributes.Count); - Assert.Contains(action.AttributeRouteModel.Attribute, action.Attributes); + selectorModel = Assert.Single(action.Selectors, a => a.AttributeRouteModel?.Template == "D"); + Assert.Contains(selectorModel.AttributeRouteModel.Attribute, action.Attributes); + } + + private IList GetAttributeRoutes(IList selectors) + { + return selectors + .Where(sm => sm.AttributeRouteModel != null) + .Select(sm => sm.AttributeRouteModel) + .ToList(); } private class BaseClassWithAttributeRoutesController @@ -1223,31 +1265,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal public MvcOptions Options { get; } - public new IEnumerable BuildControllerModels(TypeInfo typeInfo) - { - return base.BuildControllerModels(typeInfo); - } - public new ControllerModel CreateControllerModel(TypeInfo typeInfo) { return base.CreateControllerModel(typeInfo); } + public new ActionModel CreateActionModel(TypeInfo typeInfo, MethodInfo methodInfo) + { + return base.CreateActionModel(typeInfo, methodInfo); + } + public new PropertyModel CreatePropertyModel(PropertyInfo propertyInfo) { return base.CreatePropertyModel(propertyInfo); } - public new IEnumerable BuildActionModels(TypeInfo typeInfo, MethodInfo methodInfo) - { - return base.BuildActionModels(typeInfo, methodInfo); - } - - public new ActionModel CreateActionModel(MethodInfo methodInfo, IReadOnlyList attributes) - { - return base.CreateActionModel(methodInfo, attributes); - } - public new bool IsAction(TypeInfo typeInfo, MethodInfo methodInfo) { return base.IsAction(typeInfo, methodInfo);