aspnetcore/src/Microsoft.AspNet.Mvc.Core/ControllerActionDescriptorB...

819 lines
36 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.ApiExplorer;
using Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Creates instances of <see cref="ControllerActionDescriptor"/> from <see cref="ApplicationModel"/>.
/// </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="ApplicationModel"/>.
/// </summary>
/// <param name="application">The <see cref="ApplicationModel"/>.</param>
/// <returns>The list of <see cref="ControllerActionDescriptor"/>.</returns>
public static IList<ControllerActionDescriptor> Build(ApplicationModel 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)
{
// Only add properties which are explictly marked to bind.
// The attribute check is required for ModelBinder attribute.
var controllerPropertyDescriptors = controller.ControllerProperties
.Where(p => p.BindingInfo != null)
.Select(CreateParameterDescriptor)
.ToList();
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.ControllerName = controller.ControllerName;
actionDescriptor.ControllerTypeInfo = controller.ControllerType;
AddApiExplorerInfo(actionDescriptor, application, controller, action);
AddRouteConstraints(removalConstraints, actionDescriptor, controller, action);
AddProperties(actionDescriptor, action, controller, application);
actionDescriptor.BoundProperties = controllerPropertyDescriptors;
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(
ApplicationModel 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 parameterModel)
{
var parameterDescriptor = new ParameterDescriptor()
{
Name = parameterModel.ParameterName,
ParameterType = parameterModel.ParameterInfo.ParameterType,
BindingInfo = parameterModel.BindingInfo
};
return parameterDescriptor;
}
private static ParameterDescriptor CreateParameterDescriptor(PropertyModel propertyModel)
{
var parameterDescriptor = new ParameterDescriptor()
{
BindingInfo = propertyModel.BindingInfo,
Name = propertyModel.PropertyName,
ParameterType = propertyModel.PropertyInfo.PropertyType,
};
return parameterDescriptor;
}
private static void AddApiExplorerInfo(
ControllerActionDescriptor actionDescriptor,
ApplicationModel application,
ControllerModel controller,
ActionModel action)
{
var isVisible =
action.ApiExplorer?.IsVisible ??
controller.ApiExplorer?.IsVisible ??
application.ApiExplorer?.IsVisible ??
false;
var isVisibleSetOnActionOrController =
action.ApiExplorer?.IsVisible ??
controller.ApiExplorer?.IsVisible ??
false;
// ApiExplorer isn't supported on conventional-routed actions, but we still allow you to configure
// it at the application level when you have a mix of controller types. We'll just skip over enabling
// ApiExplorer for conventional-routed controllers when this happens.
var isVisibleSetOnApplication = application.ApiExplorer?.IsVisible ?? false;
if (isVisibleSetOnActionOrController && !IsAttributeRoutedAction(actionDescriptor))
{
// ApiExplorer is only supported on attribute routed actions.
throw new InvalidOperationException(Resources.FormatApiExplorer_UnsupportedAction(
actionDescriptor.DisplayName));
}
else if (isVisibleSetOnApplication && !IsAttributeRoutedAction(actionDescriptor))
{
// This is the case where we're going to be lenient, just ignore it.
}
else if (isVisible)
{
Debug.Assert(IsAttributeRoutedAction(actionDescriptor));
var apiExplorerActionData = new ApiDescriptionActionData()
{
GroupName = action.ApiExplorer?.GroupName ?? controller.ApiExplorer?.GroupName,
};
actionDescriptor.SetProperty(apiExplorerActionData);
}
}
private static void AddProperties(
ControllerActionDescriptor actionDescriptor,
ActionModel action,
ControllerModel controller,
ApplicationModel application)
{
foreach (var item in application.Properties)
{
actionDescriptor.Properties[item.Key] = item.Value;
}
foreach (var item in controller.Properties)
{
actionDescriptor.Properties[item.Key] = item.Value;
}
foreach (var item in action.Properties)
{
actionDescriptor.Properties[item.Key] = item.Value;
}
}
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(
ISet<string> removalConstraints,
ControllerActionDescriptor actionDescriptor,
ControllerModel controller,
ActionModel action)
{
// Apply all the constraints defined on the action, then controller (for example, [Area])
// to the actions. Also keep track of all the constraints that require preventing actions
// without the constraint to match. For example, actions without an [Area] attribute on their
// controller should not match when a value has been given for area when matching a url or
// generating a link.
foreach (var constraintAttribute in action.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));
}
}
}
foreach (var constraintAttribute in controller.RouteConstraints)
{
if (constraintAttribute.BlockNonAttributedActions)
{
removalConstraints.Add(constraintAttribute.RouteKey);
}
// Skip duplicates - this also means that a value on the action will take precedence
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));
}
}
}
// Lastly add the 'default' values
if (!HasConstraint(actionDescriptor.RouteConstraints, "action"))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName ?? string.Empty));
}
if (!HasConstraint(actionDescriptor.RouteConstraints, "controller"))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
controller.ControllerName));
}
}
private static bool HasConstraint(IList<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);
if (actionDescriptor.AttributeRouteInfo.Name != null)
{
actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens(
actionDescriptor.AttributeRouteInfo.Name,
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)
{
var hasAttributeRoutedActions = false;
var hasConventionallyRoutedActions = false;
var actionsForMethod = methodMap[actionDescriptor.MethodInfo];
foreach (var reflectedAction in actionsForMethod)
{
foreach (var action in reflectedAction.Value)
{
if (IsAttributeRoutedAction(action))
{
hasAttributeRoutedActions = true;
}
else
{
hasConventionallyRoutedActions = true;
}
}
}
// 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 for example:
//
// [HttpGet]
// [HttpPost("Foo")]
// public void Foo() { }
if (hasAttributeRoutedActions && hasConventionallyRoutedActions)
{
var message = CreateMixedRoutedActionDescriptorsErrorMessage(
actionDescriptor,
actionsForMethod);
routingConfigurationErrors.Add(actionDescriptor.MethodInfo, message);
}
}
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 = new List<string>();
foreach (var action in actionsForMethod.SelectMany(kvp => kvp.Value))
{
var routeTemplate = action.AttributeRouteInfo?.Template ?? nullTemplate;
var verbs = action.ActionConstraints.OfType<HttpMethodConstraint>().FirstOrDefault()?.HttpMethods;
var formattedVerbs = string.Join(", ", verbs.OrderBy(v => v, StringComparer.Ordinal));
var description =
Resources.FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod_Item(
action.DisplayName,
routeTemplate,
formattedVerbs);
actionDescriptions.Add(description);
}
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' - Route Template: 'Products' - HTTP Verbs: 'PUT'
// Action: 'MyApplication.CustomerController.Index' - Route Template: '(none)' - HTTP Verbs: 'POST'
//
// Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a route,
// or set a route template in all attributes that constrain HTTP verbs.
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);
}
}
}
}
}