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

537 lines
22 KiB
C#

// 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;
#if ASPNETCORE50
using System.Reflection;
#endif
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
{
public class ReflectedActionDescriptorProvider : IActionDescriptorProvider
{
/// <summary>
/// Represents the default order associated with this provider for dependency injection
/// purposes.
/// </summary>
public static readonly int DefaultOrder = 0;
// This is the default order for attribute routes whose order calculated from
// the reflected model is null.
private const int DefaultAttributeRouteOrder = 0;
private readonly IControllerAssemblyProvider _controllerAssemblyProvider;
private readonly IActionDiscoveryConventions _conventions;
private readonly IEnumerable<IFilter> _globalFilters;
private readonly IEnumerable<IReflectedApplicationModelConvention> _modelConventions;
private readonly IInlineConstraintResolver _constraintResolver;
public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider,
IActionDiscoveryConventions conventions,
IEnumerable<IFilter> globalFilters,
IOptionsAccessor<MvcOptions> optionsAccessor,
IInlineConstraintResolver constraintResolver)
{
_controllerAssemblyProvider = controllerAssemblyProvider;
_conventions = conventions;
_globalFilters = globalFilters ?? Enumerable.Empty<IFilter>();
_modelConventions = optionsAccessor.Options.ApplicationModelConventions;
_constraintResolver = constraintResolver;
}
public int Order
{
get { return DefaultOrder; }
}
public void Invoke(ActionDescriptorProviderContext context, Action callNext)
{
context.Results.AddRange(GetDescriptors());
callNext();
}
public IEnumerable<ReflectedActionDescriptor> GetDescriptors()
{
var model = BuildModel();
foreach (var convention in _modelConventions)
{
convention.OnModelCreated(model);
}
return Build(model);
}
public ReflectedApplicationModel BuildModel()
{
var applicationModel = new ReflectedApplicationModel();
applicationModel.Filters.AddRange(_globalFilters);
var assemblies = _controllerAssemblyProvider.CandidateAssemblies;
var types = assemblies.SelectMany(a => a.DefinedTypes);
var controllerTypes = types.Where(_conventions.IsController);
foreach (var controllerType in controllerTypes)
{
var controllerModel = new ReflectedControllerModel(controllerType);
applicationModel.Controllers.Add(controllerModel);
foreach (var methodInfo in controllerType.AsType().GetMethods())
{
var actionInfos = _conventions.GetActions(methodInfo, controllerType);
if (actionInfos == null)
{
continue;
}
foreach (var actionInfo in actionInfos)
{
var actionModel = new ReflectedActionModel(methodInfo);
actionModel.ActionName = actionInfo.ActionName;
actionModel.IsActionNameMatchRequired = actionInfo.RequireActionNameMatch;
actionModel.HttpMethods.AddRange(actionInfo.HttpMethods ?? Enumerable.Empty<string>());
foreach (var parameter in methodInfo.GetParameters())
{
actionModel.Parameters.Add(new ReflectedParameterModel(parameter));
}
controllerModel.Actions.Add(actionModel);
}
}
}
return applicationModel;
}
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
{
var actions = new List<ReflectedActionDescriptor>();
var hasAttributeRoutes = false;
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var routeTemplateErrors = new List<string>();
foreach (var controller in model.Controllers)
{
var controllerDescriptor = new ControllerDescriptor(controller.ControllerType);
foreach (var action in controller.Actions)
{
var actionDescriptor = CreateActionDescriptor(
action,
controller,
controllerDescriptor,
model.Filters);
AddActionConstraints(actionDescriptor, action, controller);
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);
}
actions.Add(actionDescriptor);
}
}
var actionsByRouteName = new Dictionary<string, IList<ActionDescriptor>>(
StringComparer.OrdinalIgnoreCase);
foreach (var actionDescriptor in actions)
{
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,
RouteKeyHandling.DenyKey));
}
// Add a route constraint with DenyKey for each constraint in the set to all the
// actions that don't have that constraint. For example, if a controller defines
// an area constraint, all actions that don't belong to an area must have a route
// constraint that prevents them from matching an incomming request.
AddRemovalConstraints(actionDescriptor, removalConstraints);
}
else
{
var attributeRouteInfo = actionDescriptor.AttributeRouteInfo;
if (attributeRouteInfo.Name != null)
{
// Build a map of attribute route name to action descriptors to ensure that all
// attribute routes with a given name have the same template.
AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
}
// We still want to add a 'null' for any constraint with DenyKey so that link generation
// works properly.
//
// Consider an action like { area = "", controller = "Home", action = "Index" }. Even if
// it's attribute routed, it needs to know that area must be null to generate a link.
foreach (var key in removalConstraints)
{
if (!actionDescriptor.RouteValueDefaults.ContainsKey(key))
{
actionDescriptor.RouteValueDefaults.Add(key, null);
}
}
}
}
var namedRoutedErrors = ValidateNamedAttributeRoutedActions(actionsByRouteName);
if (namedRoutedErrors.Any())
{
namedRoutedErrors = AddErrorNumbers(namedRoutedErrors);
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
Environment.NewLine,
string.Join(Environment.NewLine + Environment.NewLine, namedRoutedErrors));
throw new InvalidOperationException(message);
}
if (routeTemplateErrors.Any())
{
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
Environment.NewLine,
string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));
throw new InvalidOperationException(message);
}
return actions;
}
private static ReflectedActionDescriptor CreateActionDescriptor(ReflectedActionModel action,
ReflectedControllerModel controller,
ControllerDescriptor controllerDescriptor,
IEnumerable<IFilter> globalFilters)
{
var parameterDescriptors = new List<ParameterDescriptor>();
foreach (var parameter in action.Parameters)
{
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any();
var paramDescriptor = new ParameterDescriptor()
{
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional
};
if (isFromBody)
{
paramDescriptor.BodyParameterInfo = new BodyParameterInfo(
parameter.ParameterInfo.ParameterType);
}
else
{
paramDescriptor.ParameterBindingInfo = new ParameterBindingInfo(
parameter.ParameterName,
parameter.ParameterInfo.ParameterType);
}
parameterDescriptors.Add(paramDescriptor);
}
var attributeRouteInfo = CreateAttributeRouteInfo(action, controller);
var actionDescriptor = new ReflectedActionDescriptor()
{
Name = action.ActionName,
ControllerDescriptor = controllerDescriptor,
MethodInfo = action.ActionMethod,
Parameters = parameterDescriptors,
RouteConstraints = new List<RouteDataActionConstraint>(),
AttributeRouteInfo = attributeRouteInfo
};
actionDescriptor.DisplayName = string.Format(
"{0}.{1}",
action.ActionMethod.DeclaringType.FullName,
action.ActionMethod.Name);
actionDescriptor.FilterDescriptors =
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
.Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
.OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
.ToList();
return actionDescriptor;
}
private static AttributeRouteInfo CreateAttributeRouteInfo(
ReflectedActionModel action,
ReflectedControllerModel controller)
{
var combinedRoute = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(
controller.AttributeRouteModel,
action.AttributeRouteModel);
if (combinedRoute == null)
{
return null;
}
else
{
return new AttributeRouteInfo()
{
Template = combinedRoute.Template,
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
Name = combinedRoute.Name,
};
}
}
private static void AddActionConstraints(
ReflectedActionDescriptor actionDescriptor,
ReflectedActionModel action,
ReflectedControllerModel controller)
{
var httpMethods = action.HttpMethods;
if (httpMethods != null && httpMethods.Count > 0)
{
actionDescriptor.MethodConstraints = new List<HttpMethodConstraint>()
{
new HttpMethodConstraint(httpMethods)
};
}
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",
RouteKeyHandling.DenyKey));
}
}
private static void AddControllerRouteConstraints(
ReflectedActionDescriptor 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.RouteValue == null)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteKeyHandling));
}
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(ReflectedActionDescriptor 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(
ReflectedActionDescriptor actionDescriptor,
IList<string> routeTemplateErrors)
{
try
{
actionDescriptor.AttributeRouteInfo.Template = ReflectedAttributeRouteModel.ReplaceTokens(
actionDescriptor.AttributeRouteInfo.Template,
actionDescriptor.RouteValueDefaults);
}
catch (InvalidOperationException ex)
{
var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
actionDescriptor.DisplayName,
Environment.NewLine,
ex.Message);
routeTemplateErrors.Add(message);
}
}
private static void AddConstraintsAsDefaultRouteValues(ReflectedActionDescriptor 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(
ReflectedActionDescriptor actionDescriptor,
ISet<string> removalConstraints)
{
foreach (var key in removalConstraints)
{
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
key,
RouteKeyHandling.DenyKey));
}
}
}
private static void AddActionToNamedGroup(
IDictionary<string, IList<ActionDescriptor>> actionsByRouteName,
string routeName,
ReflectedActionDescriptor 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(ReflectedActionDescriptor actionDescriptor)
{
return actionDescriptor.AttributeRouteInfo != null &&
actionDescriptor.AttributeRouteInfo.Template != null;
}
private static IList<string> AddErrorNumbers(
IList<string> namedRoutedErrors)
{
return namedRoutedErrors
.Select((nre, i) =>
Resources.FormatAttributeRoute_AggregateErrorMessage_ErrorNumber(
i + 1,
Environment.NewLine,
nre))
.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 string GetRouteGroupValue(int order, string template)
{
var group = string.Format("{0}-{1}", order, template);
return ("__route__" + group).ToUpperInvariant();
}
}
}