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

353 lines
16 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 K10
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;
}
private bool HasConstraint(List<RouteDataActionConstraint> constraints, string routeKey)
{
return constraints.Any(
rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase));
}
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 parameterDescriptors = new List<ParameterDescriptor>();
foreach (var parameter in action.Parameters)
{
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any();
parameterDescriptors.Add(new ParameterDescriptor()
{
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional,
ParameterBindingInfo = isFromBody
? null
: new ParameterBindingInfo(
parameter.ParameterName,
parameter.ParameterInfo.ParameterType),
BodyParameterInfo = isFromBody
? new BodyParameterInfo(parameter.ParameterInfo.ParameterType)
: null
});
}
var combinedRoute = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(
controller.AttributeRouteModel,
action.AttributeRouteModel);
var attributeRouteInfo = combinedRoute == null ? null : new AttributeRouteInfo()
{
Template = combinedRoute.Template,
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder
};
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);
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));
}
foreach (var constraintAttribute in controller.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));
}
}
}
if (actionDescriptor.AttributeRouteInfo != null &&
actionDescriptor.AttributeRouteInfo.Template != null)
{
hasAttributeRoutes = true;
// An attribute routed action will ignore conventional routed constraints. We still
// want to provide these values as ambient values.
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);
}
}
// Replaces tokens like [controller]/[action] in the route template with the actual values
// for this action.
var templateText = actionDescriptor.AttributeRouteInfo.Template;
try
{
templateText = ReflectedAttributeRouteModel.ReplaceTokens(
templateText,
actionDescriptor.RouteValueDefaults);
}
catch (InvalidOperationException ex)
{
var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
actionDescriptor.DisplayName,
Environment.NewLine,
ex.Message);
routeTemplateErrors.Add(message);
}
actionDescriptor.AttributeRouteInfo.Template = templateText;
var routeGroupValue = GetRouteGroupValue(
actionDescriptor.AttributeRouteInfo.Order,
templateText);
var routeConstraints = new List<RouteDataActionConstraint>();
routeConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
routeGroupValue));
actionDescriptor.RouteConstraints = routeConstraints;
}
actionDescriptor.FilterDescriptors =
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
.Concat(model.Filters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
.OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
.ToList();
actions.Add(actionDescriptor);
}
}
foreach (var actionDescriptor in actions)
{
if (actionDescriptor.AttributeRouteInfo == null ||
actionDescriptor.AttributeRouteInfo.Template == null)
{
// Any any attribute routes are in use, then non-attribute-routed ADs can't be selected
// when a route group returned by the route.
if (hasAttributeRoutes)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
RouteKeyHandling.DenyKey));
}
foreach (var key in removalConstraints)
{
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
key,
RouteKeyHandling.DenyKey));
}
}
}
else
{
// We still want to add a 'null' for any constraint with DenyKey so that link generation
// works properly.
//
// Consider an action like { area = "", controller = "Home", action = "Index" }. Even if
// it's attribute routed, it needs to know that area must be null to generate a link.
foreach (var key in removalConstraints)
{
if (!actionDescriptor.RouteValueDefaults.ContainsKey(key))
{
actionDescriptor.RouteValueDefaults.Add(key, null);
}
}
}
}
if (routeTemplateErrors.Any())
{
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(
Environment.NewLine,
string.Join(Environment.NewLine + Environment.NewLine, routeTemplateErrors));
throw new InvalidOperationException(message);
}
return actions;
}
private static string GetRouteGroupValue(int order, string template)
{
var group = string.Format("{0}-{1}", order, template);
return ("__route__" + group).ToUpperInvariant();
}
}
}