Refactoring the ControllerActionDescriptorProvider

This commit is contained in:
Ryan Nowak 2014-10-21 11:16:52 -07:00
parent 756f8be49c
commit 7ae5e66ccd
21 changed files with 2487 additions and 2646 deletions

View File

@ -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; }
}
}

View File

@ -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
{
/// <summary>
/// Applies conventions to a <see cref="GlobalModel"/>.
/// </summary>
public static class ApplicationModelConventions
{
/// <summary>
/// Applies conventions to a <see cref="GlobalModel"/>.
/// </summary>
/// <param name="applicationModel">The <see cref="GlobalModel"/>.</param>
/// <param name="conventions">The set of conventions.</param>
public static void ApplyConventions(
[NotNull] GlobalModel applicationModel,
[NotNull] IEnumerable<IGlobalModelConvention> 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<IControllerModelConvention>()
.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<IActionModelConvention>()
.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<IParameterModelConvention>()
.ToArray();
foreach (var parameterConvention in parameterConventions)
{
parameterConvention.Apply(parameter);
}
}
}
}
}
}
}

View File

@ -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
{
/// <summary>
/// A default implementation of <see cref="IActionModelBuilder"/>.
/// </summary>
public class DefaultActionModelBuilder : IActionModelBuilder
{
/// <inheritdoc />
public IEnumerable<ActionModel> BuildActionModels([NotNull] MethodInfo methodInfo)
{
if (!IsAction(methodInfo))
{
return Enumerable.Empty<ActionModel>();
}
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToArray() is object
var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType<object>().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<object>();
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<ActionModel>();
if (splitAttributes.Count == 0 && !hasSilentRouteAttribute)
{
actionModels.Add(CreateActionModel(methodInfo, attributes));
}
else
{
foreach (var splitAttribute in splitAttributes)
{
var filteredAttributes = new List<object>();
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<object>();
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;
}
/// <summary>
/// Returns <c>true</c> if the <paramref name="methodInfo"/> is an action. Otherwise <c>false</c>.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/>.</param>
/// <returns><c>true</c> if the <paramref name="methodInfo"/> is an action. Otherwise <c>false</c>.</returns>
/// <remarks>
/// Override this method to provide custom logic to determine which methods are considered actions.
/// </remarks>
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);
}
/// <summary>
/// Creates an <see cref="ActionModel"/> for the given <see cref="MethodInfo"/>.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/>.</param>
/// <param name="attributes">The set of attributes to use as metadata.</param>
/// <returns>An <see cref="ActionModel"/> for the given <see cref="MethodInfo"/>.</returns>
/// <remarks>
/// An action-method in code may expand into multiple <see cref="ActionModel"/> 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 <paramref name="attributes"/> list to find metadata related to
/// the action being created.
/// </remarks>
protected virtual ActionModel CreateActionModel(
[NotNull] MethodInfo methodInfo,
[NotNull] IReadOnlyList<object> attributes)
{
var actionModel = new ActionModel(methodInfo)
{
IsActionNameMatchRequired = true,
};
actionModel.Attributes.AddRange(attributes);
actionModel.ActionConstraints.AddRange(attributes.OfType<IActionConstraintMetadata>());
actionModel.Filters.AddRange(attributes.OfType<IFilter>());
var actionName = attributes.OfType<ActionNameAttribute>().FirstOrDefault();
if (actionName?.Name != null)
{
actionModel.ActionName = actionName.Name;
}
else
{
actionModel.ActionName = methodInfo.Name;
}
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiVisibility != null)
{
actionModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
}
var apiGroupName = attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiGroupName != null)
{
actionModel.ApiExplorerGroupName = apiGroupName.GroupName;
}
var httpMethods = attributes.OfType<IActionHttpMethodProvider>();
actionModel.HttpMethods.AddRange(
httpMethods
.Where(a => a.HttpMethods != null)
.SelectMany(a => a.HttpMethods)
.Distinct());
var routeTemplateProvider = attributes.OfType<IRouteTemplateProvider>().FirstOrDefault();
if (routeTemplateProvider != null && !IsSilentRouteAttribute(routeTemplateProvider))
{
actionModel.AttributeRouteModel = new AttributeRouteModel(routeTemplateProvider);
}
return actionModel;
}
/// <summary>
/// Creates a <see cref="ParameterModel"/> for the given <see cref="ParameterInfo"/>.
/// </summary>
/// <param name="parameterInfo">The <see cref="ParameterInfo"/>.</param>
/// <returns>A <see cref="ParameterModel"/> for the given <see cref="ParameterInfo"/>.</returns>
protected virtual ParameterModel CreateParameterModel([NotNull] ParameterInfo parameterInfo)
{
var parameterModel = new ParameterModel(parameterInfo);
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToArray() is object
var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType<object>().ToArray();
parameterModel.Attributes.AddRange(attributes);
parameterModel.BinderMetadata = attributes.OfType<IBinderMetadata>().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;
}
}
}

View File

@ -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
{
/// <summary>
/// A default implementation of <see cref="IControllerModelBuilder"/>.
/// </summary>
public class DefaultControllerModelBuilder : IControllerModelBuilder
{
private readonly IActionModelBuilder _actionModelBuilder;
/// <summary>
/// Creates a new <see cref="DefaultControllerModelBuilder"/>.
/// </summary>
/// <param name="actionModelBuilder">The <see cref="IActionModelBuilder"/> used to create actions.</param>
public DefaultControllerModelBuilder(IActionModelBuilder actionModelBuilder)
{
_actionModelBuilder = actionModelBuilder;
}
/// <inheritdoc />
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;
}
/// <summary>
/// Returns <c>true</c> if the <paramref name="typeInfo"/> is a controller. Otherwise <c>false</c>.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
/// <returns><c>true</c> if the <paramref name="typeInfo"/> is a controller. Otherwise <c>false</c>.</returns>
/// <remarks>
/// Override this method to provide custom logic to determine which types are considered controllers.
/// </remarks>
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);
}
/// <summary>
/// Creates an <see cref="ControllerModel"/> for the given <see cref="TypeInfo"/>.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
/// <returns>A <see cref="ControllerModel"/> for the given <see cref="TypeInfo"/>.</returns>
protected virtual ControllerModel CreateControllerModel([NotNull] TypeInfo typeInfo)
{
var controllerModel = new ControllerModel(typeInfo);
// CoreCLR returns IEnumerable<Attribute> from GetCustomAttributes - the OfType<object>
// is needed to so that the result of ToArray() is object
var attributes = typeInfo.GetCustomAttributes(inherit: true).OfType<object>().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<IActionConstraintMetadata>());
controllerModel.Filters.AddRange(attributes.OfType<IFilter>());
controllerModel.RouteConstraints.AddRange(attributes.OfType<RouteConstraintAttribute>());
controllerModel.AttributeRoutes.AddRange(
attributes.OfType<IRouteTemplateProvider>().Select(rtp => new AttributeRouteModel(rtp)));
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiVisibility != null)
{
controllerModel.ApiExplorerIsVisible = !apiVisibility.IgnoreApi;
}
var apiGroupName = attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiGroupName != null)
{
controllerModel.ApiExplorerGroupName = apiGroupName.GroupName;
}
return controllerModel;
}
}
}

View File

@ -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
{
/// <summary>
/// Creates a set of <see cref="ActionModel"/> for a method.
/// </summary>
public interface IActionModelBuilder
{
/// <summary>
/// Creates a set of <see cref="ActionModel"/> for a method. May return null or empty if
/// <paramref name="methodInfo"/> is not an action method.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/>.</param>
/// <returns>A set of <see cref="ActionModel"/> or null.</returns>
/// <remarks>
/// Instances of <see cref="ActionModel"/> returned from this interface should have their
/// <see cref="ActionModel.Parameters"/> initialized.
/// </remarks>
IEnumerable<ActionModel> BuildActionModels([NotNull] MethodInfo methodInfo);
}
}

View File

@ -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
{
/// <summary>
/// Creates a set of <see cref="ControllerModel"/> for a type.
/// </summary>
public interface IControllerModelBuilder
{
/// <summary>
/// Creates a set of <see cref="ControllerModel"/> for a type. May return null or empty if
/// <paramref name="typeInfo"/> is not a controller type.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
/// <returns>A <see cref="ControllerModel"/> or null.</returns>
/// <remarks>
/// Instances of <see cref="ControllerModel"/> returned from this interface should have their
/// <see cref="ControllerModel.Actions"/> initialized.
/// </remarks>
ControllerModel BuildControllerModel([NotNull] TypeInfo typeInfo);
}
}

View File

@ -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
{
/// <summary>
/// Creates instances of <see cref="ControllerActionDescriptor"/> from <see cref="GlobalModel"/>.
/// </summary>
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;
/// <summary>
/// Creates instances of <see cref="ControllerActionDescriptor"/> from <see cref="GlobalModel"/>.
/// </summary>
/// <param name="application">The <see cref="GlobalModel"/>.</param>
/// <returns>The list of <see cref="ControllerActionDescriptor"/>.</returns>
public static IList<ControllerActionDescriptor> Build(GlobalModel application)
{
var actions = new List<ControllerActionDescriptor>();
var hasAttributeRoutes = false;
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var methodInfoMap = new MethodToActionMap();
var routeTemplateErrors = new List<string>();
var attributeRoutingConfigurationErrors = new Dictionary<MethodInfo, string>();
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<string, IList<ActionDescriptor>>(
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<MethodInfo>();
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<ControllerActionDescriptor> CreateActionDescriptors(
GlobalModel application,
ControllerModel controller,
ActionModel action)
{
var actionDescriptors = new List<ControllerActionDescriptor>();
// 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<ParameterDescriptor>();
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<RouteDataActionConstraint>(),
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<IFilter> actionFilters,
IEnumerable<IFilter> controllerFilters,
IEnumerable<IFilter> 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<IActionConstraintMetadata> controllerConstraints)
{
var constraints = new List<IActionConstraintMetadata>();
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<RouteConstraintAttribute> routeconstraints,
ISet<string> removalConstraints)
{
// Apply all the constraints defined on the controller (for example, [Area]) to the actions
// in that controller. Also keep track of all the constraints that require preventing actions
// 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<RouteDataActionConstraint> 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<RouteDataActionConstraint>();
routeConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
routeGroupValue));
actionDescriptor.RouteConstraints = routeConstraints;
}
private static void ReplaceAttributeRouteTokens(
ControllerActionDescriptor actionDescriptor,
IList<string> 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<string> removalConstraints)
{
foreach (var key in removalConstraints)
{
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
key,
string.Empty));
}
}
}
private static void AddActionToNamedGroup(
IDictionary<string, IList<ActionDescriptor>> actionsByRouteName,
string routeName,
ControllerActionDescriptor actionDescriptor)
{
IList<ActionDescriptor> namedActionGroup;
if (actionsByRouteName.TryGetValue(routeName, out namedActionGroup))
{
namedActionGroup.Add(actionDescriptor);
}
else
{
namedActionGroup = new List<ActionDescriptor>();
namedActionGroup.Add(actionDescriptor);
actionsByRouteName.Add(routeName, namedActionGroup);
}
}
private static bool IsAttributeRoutedAction(ControllerActionDescriptor actionDescriptor)
{
return actionDescriptor.AttributeRouteInfo?.Template != null;
}
private static IList<string> AddErrorNumbers(
IEnumerable<string> namedRoutedErrors)
{
return namedRoutedErrors
.Select((error, i) =>
Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
i + 1,
Environment.NewLine,
error))
.ToList();
}
private static IList<string> ValidateNamedAttributeRoutedActions(
IDictionary<string,
IList<ActionDescriptor>> actionsGroupedByRouteName)
{
var namedRouteErrors = new List<string>();
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<MethodInfo, IDictionary<ActionModel, IList<ControllerActionDescriptor>>> methodMap,
ControllerActionDescriptor actionDescriptor,
IDictionary<MethodInfo, string> routingConfigurationErrors)
{
string combinedErrorMessage = null;
var hasAttributeRoutedActions = false;
var hasConventionallyRoutedActions = false;
var invalidHttpMethodActions = new Dictionary<ActionModel, IEnumerable<string>>();
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<ActionModel, IEnumerable<string>> 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<ActionModel, IEnumerable<string>> invalidHttpMethodActions,
IDictionary<ActionModel, IList<ControllerActionDescriptor>> actionsForMethod)
{
var messagesForMethodInfo = new List<string>();
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<ActionModel, IList<ControllerActionDescriptor>> 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<string> 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<MethodInfo, IDictionary<ActionModel, IList<ControllerActionDescriptor>>>
{
public void AddToMethodInfo(ActionModel action,
IList<ControllerActionDescriptor> actionDescriptors)
{
IDictionary<ActionModel, IList<ControllerActionDescriptor>> actionsForMethod = null;
if (TryGetValue(action.ActionMethod, out actionsForMethod))
{
actionsForMethod.Add(action, actionDescriptors);
}
else
{
var reflectedActionMap =
new Dictionary<ActionModel, IList<ControllerActionDescriptor>>();
reflectedActionMap.Add(action, actionDescriptors);
Add(action.ActionMethod, reflectedActionMap);
}
}
}
}
}

View File

@ -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<ActionInfo> 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;
}
/// <summary>
/// Determines whether the method is a valid action.
/// </summary>
/// <param name="method">The <see cref="MethodInfo"/>.</param>
/// <returns>true if the method is a valid action. Otherwise, false.</returns>
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<object>().ToArray();
var actionNameAttribute = attributes.OfType<ActionNameAttribute>().FirstOrDefault();
var httpMethodConstraints = attributes.OfType<IActionHttpMethodProvider>();
var routeTemplates = attributes.OfType<IRouteTemplateProvider>();
return new ActionAttributes()
{
Attributes = attributes,
ActionNameAttribute = actionNameAttribute,
HttpMethodProviderAttributes = httpMethodConstraints,
RouteTemplateProviderAttributes = routeTemplates,
};
}
private IEnumerable<ActionInfo> 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<ActionInfo>();
}
}
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<IRouteTemplateProvider>()
.Any(cr => cr.Template != null);
}
private static IEnumerable<ActionInfo> 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<ActionInfo> GetAttributeRoutedActions(
ActionAttributes actionAttributes,
string actionName)
{
var actions = new List<ActionInfo>();
// 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<IActionHttpMethodProvider> HttpMethodProviderAttributes { get; set; }
public IEnumerable<IRouteTemplateProvider> RouteTemplateProviderAttributes { get; set; }
public bool HasSpecialAttribute()
{
return ActionNameAttribute != null ||
HttpMethodProviderAttributes.Any() ||
RouteTemplateProviderAttributes.Any();
}
}
}
}

View File

@ -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<ActionInfo> GetActions(MethodInfo methodInfo, TypeInfo controllerTypeInfo);
}
}

View File

@ -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<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
// These are consumed only when creating action descriptors, then they can be de-allocated
yield return describe.Transient<IControllerModelBuilder, DefaultControllerModelBuilder>();
yield return describe.Transient<IActionModelBuilder, DefaultActionModelBuilder>();
// This accesses per-request services to activate the controller
yield return describe.Transient<IControllerFactory, DefaultControllerFactory>();
// This has a cache, so it needs to be a singleton
yield return describe.Singleton<IControllerActivator, DefaultControllerActivator>();
// This accesses per-reqest services
yield return describe.Transient<IActionInvokerFactory, ActionInvokerFactory>();
// This provider needs access to the per-request services, but might be used many times for a given

View File

@ -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<string, object>
{
{ "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<string, object>()
{
{ "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<string, object>()
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "controller", "ActionName" },
{ "action", actionName }
};
// Act
var result = await InvokeActionSelector(routeContext);
// Assert
Assert.Equal(actionName, result.Name);
}
private async Task<ActionDescriptor> InvokeActionSelector(
RouteContext context,
IActionDiscoveryConventions actionDiscoveryConventions = null)
{
var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
var descriptorProvider =
new NestedProviderManager<ActionDescriptorProviderContext>(new[] { actionDescriptorProvider });
var serviceContainer = new ServiceContainer();
serviceContainer.AddService(typeof(INestedProviderManager<ActionDescriptorProviderContext>),
descriptorProvider);
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var actionConstraintProvider = new NestedProviderManager<ActionConstraintProviderContext>(
new INestedProvider<ActionConstraintProviderContext>[]
{
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<HttpRequest>();
var headers = new Mock<IHeaderDictionary>();
request.SetupGet(r => r.Headers).Returns(headers.Object);
request.SetupGet(x => x.Method).Returns(httpMethod);
var httpContext = new Mock<HttpContext>();
httpContext.SetupGet(c => c.Request).Returns(request.Object);
return httpContext.Object;
}
private class CustomActionConvention : DefaultActionDiscoveryConventions
{
public override IEnumerable<ActionInfo> GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo)
{
var actions = new List<ActionInfo>(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

View File

@ -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<CustomHttpMethodsAttribute>(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<HttpDeleteAttribute>(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<HttpPostAttribute>(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<RouteAttribute>(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<AcceptVerbsAttribute>(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<HttpPostAttribute>(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<HttpGetAttribute>(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<HttpGetAttribute>(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<T>()
{
}
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<string> HttpMethods
{
get
{
return _methods;
}
}
}
}
}

View File

@ -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<string>).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<T>
{
}
public class DerivedGenericController : OpenGenericController<string>
{
}
public interface IController
{
}
public class NoSuffix : Mvc.Controller
{
}
public class PocoController
{
}
}

View File

@ -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<IFilter> filters = null)
{
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfo);
var assemblyProvider = new Mock<IAssemblyProvider>();
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<IAssemblyProvider>();
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<MvcOptions> options)
{
var conventions = new StaticActionDiscoveryConventions(type);
var modelBuilder = new StaticControllerModelBuilder(type);
var assemblyProvider = new Mock<IAssemblyProvider>();
assemblyProvider
@ -1354,14 +1352,14 @@ namespace Microsoft.AspNet.Mvc.Test
return new ControllerActionDescriptorProvider(
assemblyProvider.Object,
conventions,
modelBuilder,
new TestGlobalFilterProvider(),
options);
}
private IEnumerable<ActionDescriptor> GetDescriptors(params TypeInfo[] controllerTypeInfos)
{
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
var modelBuilder = new StaticControllerModelBuilder(controllerTypeInfos);
var assemblyProvider = new Mock<IAssemblyProvider>();
assemblyProvider
@ -1370,7 +1368,7 @@ namespace Microsoft.AspNet.Mvc.Test
var provider = new ControllerActionDescriptorProvider(
assemblyProvider.Object,
conventions,
modelBuilder,
new TestGlobalFilterProvider(),
new MockMvcOptionsAccessor());

View File

@ -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<string, object>
{
{ "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<string, object>
{
{ "controller", "RpcOnly" }
};
// Act
var result = await InvokeActionSelector(routeContext, new CustomActionConvention());
// Assert
Assert.Equal("PostSomething", result.Name);
}
private async Task<ActionDescriptor> 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<ActionDescriptor> InvokeActionSelector(RouteContext context,
DefaultActionDiscoveryConventions actionDiscoveryConventions)
{
var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
var descriptorProvider =
new NestedProviderManager<ActionDescriptorProviderContext>(new[] { actionDescriptorProvider });
var serviceContainer = new ServiceContainer();
serviceContainer.AddService(typeof(INestedProviderManager<ActionDescriptorProviderContext>),
descriptorProvider);
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var actionConstraintProvider = new NestedProviderManager<ActionConstraintProviderContext>(
new INestedProvider<ActionConstraintProviderContext>[]
{
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<IAssemblyProvider>();
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<HttpRequest>();
var headers = new Mock<IHeaderDictionary>();
request.SetupGet(r => r.Headers).Returns(headers.Object);
request.SetupGet(x => x.Method).Returns(httpMethod);
var httpContext = new Mock<HttpContext>();
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<ActionInfo> GetActions([NotNull]MethodInfo methodInfo, [NotNull]TypeInfo controllerTypeInfo)
{
var actions = new List<ActionInfo>(
base.GetActions(methodInfo, controllerTypeInfo) ??
new List<ActionInfo>());
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

View File

@ -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<CustomHttpMethodsAttribute>(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<HttpDeleteAttribute>(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<HttpPostAttribute>(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<RouteAttribute>(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<AcceptVerbsAttribute>(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<HttpPostAttribute>(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<HttpGetAttribute>(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<HttpGetAttribute>(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<string>).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<T>()
{
}
private void PrivateMethod()
{
}
public static void StaticMethod()
{
}
protected static void ProtectedStaticMethod()
{
}
private static void PrivateStaticMethod()
{
}
}
public class Controller
{
}
public class OpenGenericController<T>
{
}
public class DerivedGenericController : OpenGenericController<string>
{
}
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<string> HttpMethods
{
get
{
return _methods;
}
}
}
}

View File

@ -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<string, object>
{
{ "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<string, object>()
{
{ "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<string, object>()
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "controller", "ActionName" },
{ "action", actionName }
};
// Act
var result = await InvokeActionSelector(routeContext);
// Assert
Assert.Equal(actionName, result.Name);
}
private async Task<ActionDescriptor> InvokeActionSelector(RouteContext context)
{
var actionDescriptorProvider = GetActionDescriptorProvider();
var descriptorProvider =
new NestedProviderManager<ActionDescriptorProviderContext>(new[] { actionDescriptorProvider });
var serviceContainer = new ServiceContainer();
serviceContainer.AddService(typeof(INestedProviderManager<ActionDescriptorProviderContext>),
descriptorProvider);
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
var actionConstraintProvider = new NestedProviderManager<ActionConstraintProviderContext>(
new INestedProvider<ActionConstraintProviderContext>[]
{
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
{
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc
{
get
{
yield return typeof(StaticActionDiscoveryConventions).GetTypeInfo().Assembly;
yield return typeof(StaticAssemblyProvider).GetTypeInfo().Assembly;
}
}
}

View File

@ -5,22 +5,23 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
namespace Microsoft.AspNet.Mvc.ApplicationModel
{
/// <summary>
/// An implementation of DefaultActionDiscoveryConventions that only allows controllers
/// An implementation of StaticControllerModelBuilder that only allows controllers
/// from a fixed set of types.
/// </summary>
public class StaticActionDiscoveryConventions : DefaultActionDiscoveryConventions
public class StaticControllerModelBuilder : DefaultControllerModelBuilder
{
public StaticActionDiscoveryConventions(params TypeInfo[] controllerTypes)
public StaticControllerModelBuilder(params TypeInfo[] controllerTypes)
: base(new DefaultActionModelBuilder())
{
ControllerTypes = new List<TypeInfo>(controllerTypes ?? Enumerable.Empty<TypeInfo>());
}
public List<TypeInfo> ControllerTypes { get; private set; }
public override bool IsController([NotNull]TypeInfo typeInfo)
protected override bool IsController([NotNull] TypeInfo typeInfo)
{
return ControllerTypes.Contains(typeInfo);
}

View File

@ -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<IFilter>());
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" &&