// 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 { /// /// Represents the default order associated with this provider for dependency injection /// purposes. /// 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 _globalFilters; private readonly IEnumerable _modelConventions; private readonly IInlineConstraintResolver _constraintResolver; public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider, IActionDiscoveryConventions conventions, IEnumerable globalFilters, IOptionsAccessor optionsAccessor, IInlineConstraintResolver constraintResolver) { _controllerAssemblyProvider = controllerAssemblyProvider; _conventions = conventions; _globalFilters = globalFilters ?? Enumerable.Empty(); _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 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()); foreach (var parameter in methodInfo.GetParameters()) { actionModel.Parameters.Add(new ReflectedParameterModel(parameter)); } controllerModel.Actions.Add(actionModel); } } } return applicationModel; } private bool HasConstraint(List constraints, string routeKey) { return constraints.Any( rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase)); } public List Build(ReflectedApplicationModel model) { var actions = new List(); var hasAttributeRoutes = false; var removalConstraints = new HashSet(StringComparer.OrdinalIgnoreCase); var routeTemplateErrors = new List(); foreach (var controller in model.Controllers) { var controllerDescriptor = new ControllerDescriptor(controller.ControllerType); foreach (var action in controller.Actions) { var parameterDescriptors = new List(); foreach (var parameter in action.Parameters) { var isFromBody = parameter.Attributes.OfType().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(), 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() { 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(); 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(); } } }