// 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.Linq; using System.Reflection; using Microsoft.AspNet.Mvc.ActionConstraints; using Microsoft.AspNet.Mvc.Actions; using Microsoft.AspNet.Mvc.ApiExplorer; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; using Microsoft.Framework.Internal; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Mvc.ApplicationModels { public class DefaultApplicationModelProvider : IApplicationModelProvider { private readonly ICollection _globalFilters; public DefaultApplicationModelProvider(IOptions mvcOptionsAccessor) { _globalFilters = mvcOptionsAccessor.Value.Filters; } /// public int Order { get { return -1000; } } /// public virtual void OnProvidersExecuting([NotNull] ApplicationModelProviderContext context) { foreach (var filter in _globalFilters) { context.Result.Filters.Add(filter); } foreach (var controllerType in context.ControllerTypes) { var controllerModels = BuildControllerModels(controllerType); if (controllerModels != null) { foreach (var controllerModel in controllerModels) { context.Result.Controllers.Add(controllerModel); controllerModel.Application = context.Result; foreach (var propertyHelper in PropertyHelper.GetProperties(controllerType.AsType())) { var propertyInfo = propertyHelper.Property; var propertyModel = CreatePropertyModel(propertyInfo); if (propertyModel != null) { propertyModel.Controller = controllerModel; controllerModel.ControllerProperties.Add(propertyModel); } } foreach (var methodInfo in controllerType.AsType().GetMethods()) { var actionModels = BuildActionModels(controllerType, methodInfo); if (actionModels != null) { foreach (var actionModel in actionModels) { actionModel.Controller = controllerModel; controllerModel.Actions.Add(actionModel); foreach (var parameterInfo in actionModel.ActionMethod.GetParameters()) { var parameterModel = CreateParameterModel(parameterInfo); if (parameterModel != null) { parameterModel.Action = actionModel; actionModel.Parameters.Add(parameterModel); } } } } } } } } } /// public virtual void OnProvidersExecuted([NotNull] ApplicationModelProviderContext context) { // Intentionally empty. } /// /// Creates the instances for the given controller . /// /// The controller . /// /// A set of instances for the given controller or /// null if the does not represent a controller. /// protected virtual IEnumerable BuildControllerModels([NotNull] TypeInfo typeInfo) { var controllerModel = CreateControllerModel(typeInfo); yield return controllerModel; } /// /// Creates a for the given . /// /// The . /// A for the given . protected virtual ControllerModel CreateControllerModel([NotNull] TypeInfo typeInfo) { // For attribute routes on a controller, we want want to support 'overriding' routes on a derived // class. So we need to walk up the hierarchy looking for the first class to define routes. // // Then we want to 'filter' the set of attributes, so that only the effective routes apply. var currentTypeInfo = typeInfo; var objectTypeInfo = typeof(object).GetTypeInfo(); IRouteTemplateProvider[] routeAttributes = null; do { routeAttributes = currentTypeInfo .GetCustomAttributes(inherit: false) .OfType() .ToArray(); if (routeAttributes.Length > 0) { // Found 1 or more route attributes. break; } currentTypeInfo = currentTypeInfo.BaseType.GetTypeInfo(); } while (currentTypeInfo != objectTypeInfo); // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType // is needed to so that the result of ToArray() is object var attributes = typeInfo.GetCustomAttributes(inherit: true).OfType().ToArray(); // This is fairly complicated so that we maintain referential equality between items in // ControllerModel.Attributes and ControllerModel.Attributes[*].Attribute. var filteredAttributes = new List(); foreach (var attribute in attributes) { if (attribute is IRouteTemplateProvider) { // This attribute is a route-attribute, leave it out. } else { filteredAttributes.Add(attribute); } } filteredAttributes.AddRange(routeAttributes); attributes = filteredAttributes.ToArray(); var controllerModel = new ControllerModel(typeInfo, attributes); AddRange( controllerModel.AttributeRoutes, routeAttributes.Select(a => new AttributeRouteModel(a))); controllerModel.ControllerName = typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? typeInfo.Name.Substring(0, typeInfo.Name.Length - "Controller".Length) : typeInfo.Name; AddRange(controllerModel.ActionConstraints, attributes.OfType()); AddRange(controllerModel.Filters, attributes.OfType()); AddRange(controllerModel.RouteConstraints, attributes.OfType()); var apiVisibility = attributes.OfType().FirstOrDefault(); if (apiVisibility != null) { controllerModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; } var apiGroupName = attributes.OfType().FirstOrDefault(); if (apiGroupName != null) { controllerModel.ApiExplorer.GroupName = apiGroupName.GroupName; } // Controllers can implement action filter and result filter interfaces. We add // a special delegating filter implementation to the pipeline to handle it. // // This is needed because filters are instantiated before the controller. if (typeof(IAsyncActionFilter).GetTypeInfo().IsAssignableFrom(typeInfo) || typeof(IActionFilter).GetTypeInfo().IsAssignableFrom(typeInfo)) { controllerModel.Filters.Add(new ControllerActionFilter()); } if (typeof(IAsyncResultFilter).GetTypeInfo().IsAssignableFrom(typeInfo) || typeof(IResultFilter).GetTypeInfo().IsAssignableFrom(typeInfo)) { controllerModel.Filters.Add(new ControllerResultFilter()); } return controllerModel; } /// /// Creates a for the given . /// /// The . /// A for the given . protected virtual PropertyModel CreatePropertyModel([NotNull] PropertyInfo propertyInfo) { // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType // is needed to so that the result of ToArray() is object var attributes = propertyInfo.GetCustomAttributes(inherit: true).OfType().ToArray(); var propertyModel = new PropertyModel(propertyInfo, attributes); var bindingInfo = BindingInfo.GetBindingInfo(attributes); propertyModel.BindingInfo = bindingInfo; propertyModel.PropertyName = propertyInfo.Name; return propertyModel; } /// /// Creates the instances for the given action . /// /// The controller . /// The action . /// /// A set of instances for the given action or /// null if the does not represent an action. /// protected virtual IEnumerable BuildActionModels( [NotNull] TypeInfo typeInfo, [NotNull] MethodInfo methodInfo) { if (!IsAction(typeInfo, methodInfo)) { return Enumerable.Empty(); } // For attribute routes on a action, we want want to support 'overriding' routes on a // virtual method, but allow 'overriding'. So we need to walk up the hierarchy looking // for the first definition to define routes. // // Then we want to 'filter' the set of attributes, so that only the effective routes apply. var currentMethodInfo = methodInfo; IRouteTemplateProvider[] routeAttributes = null; while (true) { routeAttributes = currentMethodInfo .GetCustomAttributes(inherit: false) .OfType() .ToArray(); if (routeAttributes.Length > 0) { // Found 1 or more route attributes. break; } // GetBaseDefinition returns 'this' when it gets to the bottom of the chain. var nextMethodInfo = currentMethodInfo.GetBaseDefinition(); if (currentMethodInfo == nextMethodInfo) { break; } currentMethodInfo = nextMethodInfo; } // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType // is needed to so that the result of ToArray() is object var attributes = methodInfo.GetCustomAttributes(inherit: true).OfType().ToArray(); // This is fairly complicated so that we maintain referential equality between items in // ActionModel.Attributes and ActionModel.Attributes[*].Attribute. var applicableAttributes = new List(); foreach (var attribute in attributes) { if (attribute is IRouteTemplateProvider) { // This attribute is a route-attribute, leave it out. } else { applicableAttributes.Add(attribute); } } applicableAttributes.AddRange(routeAttributes); attributes = applicableAttributes.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 an action for each attribute that 'defines' a route, and a single action // for all of the ones that don't (if any exist). // // If the attribute that 'defines' a route is NOT an IActionHttpMethodProvider, then we'll include with // it, any IActionHttpMethodProvider that are 'silent' IRouteTemplateProviders. In this case the 'extra' // action for silent route providers isn't needed. // // Ex: // [HttpGet] // [AcceptVerbs("POST", "PUT")] // [HttpPost("Api/Things")] // public void DoThing() // // This will generate 2 actions: // 1. [HttpPost("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 routeProviders = new List(); var createActionForSilentRouteProviders = false; foreach (var attribute in attributes) { var routeTemplateProvider = attribute as IRouteTemplateProvider; if (routeTemplateProvider != null) { if (IsSilentRouteAttribute(routeTemplateProvider)) { createActionForSilentRouteProviders = true; } else { routeProviders.Add(attribute); } } } foreach (var routeProvider in routeProviders) { // If we see an attribute like // [Route(...)] // // Then we want to group any attributes like [HttpGet] with it. // // Basically... // // [HttpGet] // [HttpPost("Products")] // public void Foo() { } // // Is two actions. And... // // [HttpGet] // [Route("Products")] // public void Foo() { } // // Is one action. if (!(routeProvider is IActionHttpMethodProvider)) { createActionForSilentRouteProviders = false; } } var actionModels = new List(); if (routeProviders.Count == 0 && !createActionForSilentRouteProviders) { actionModels.Add(CreateActionModel(methodInfo, attributes)); } else { // Each of these routeProviders are the ones that actually have routing information on them // something like [HttpGet] won't show up here, but [HttpGet("Products")] will. foreach (var routeProvider in routeProviders) { var filteredAttributes = new List(); foreach (var attribute in attributes) { if (attribute == routeProvider) { filteredAttributes.Add(attribute); } else if (routeProviders.Contains(attribute)) { // Exclude other route template providers } else if ( routeProvider is IActionHttpMethodProvider && attribute is IActionHttpMethodProvider) { // Exclude other http method providers if this route is an // http method provider. } else { filteredAttributes.Add(attribute); } } actionModels.Add(CreateActionModel(methodInfo, filteredAttributes)); } if (createActionForSilentRouteProviders) { var filteredAttributes = new List(); foreach (var attribute in attributes) { if (!routeProviders.Contains(attribute)) { filteredAttributes.Add(attribute); } } actionModels.Add(CreateActionModel(methodInfo, filteredAttributes)); } } return actionModels; } /// /// Returns true if the is an action. Otherwise false. /// /// The . /// The . /// true if the is an action. Otherwise false. /// /// Override this method to provide custom logic to determine which methods are considered actions. /// protected virtual bool IsAction([NotNull] TypeInfo typeInfo, [NotNull] MethodInfo methodInfo) { // 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). if (methodInfo.IsSpecialName) { return false; } if (methodInfo.IsDefined(typeof(NonActionAttribute))) { return false; } // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) { return false; } // Dispose method implemented from IDisposable is not valid if (IsIDisposableMethod(methodInfo, typeInfo)) { return false; } if (methodInfo.IsStatic) { return false; } if (methodInfo.IsAbstract) { return false; } if (methodInfo.IsConstructor) { return false; } if (methodInfo.IsGenericMethod) { return false; } return methodInfo.IsPublic; } /// /// Creates an for the given . /// /// The . /// The set of attributes to use as metadata. /// An for the given . /// /// An action-method in code may expand into multiple 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 list to find metadata related to /// the action being created. /// protected virtual ActionModel CreateActionModel( [NotNull] MethodInfo methodInfo, [NotNull] IReadOnlyList attributes) { var actionModel = new ActionModel(methodInfo, attributes); AddRange(actionModel.ActionConstraints, attributes.OfType()); AddRange(actionModel.Filters, attributes.OfType()); var actionName = attributes.OfType().FirstOrDefault(); if (actionName?.Name != null) { actionModel.ActionName = actionName.Name; } else { actionModel.ActionName = methodInfo.Name; } var apiVisibility = attributes.OfType().FirstOrDefault(); if (apiVisibility != null) { actionModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; } var apiGroupName = attributes.OfType().FirstOrDefault(); if (apiGroupName != null) { actionModel.ApiExplorer.GroupName = apiGroupName.GroupName; } var httpMethods = attributes.OfType(); AddRange(actionModel.HttpMethods, httpMethods .Where(a => a.HttpMethods != null) .SelectMany(a => a.HttpMethods) .Distinct()); AddRange(actionModel.RouteConstraints, attributes.OfType()); var routeTemplateProvider = attributes .OfType() .Where(a => !IsSilentRouteAttribute(a)) .SingleOrDefault(); if (routeTemplateProvider != null) { actionModel.AttributeRouteModel = new AttributeRouteModel(routeTemplateProvider); } return actionModel; } /// /// Creates a for the given . /// /// The . /// A for the given . protected virtual ParameterModel CreateParameterModel([NotNull] ParameterInfo parameterInfo) { // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType // is needed to so that the result of ToArray() is object var attributes = parameterInfo.GetCustomAttributes(inherit: true).OfType().ToArray(); var parameterModel = new ParameterModel(parameterInfo, attributes); var bindingInfo = BindingInfo.GetBindingInfo(attributes); parameterModel.BindingInfo = bindingInfo; parameterModel.ParameterName = parameterInfo.Name; return parameterModel; } private bool IsIDisposableMethod(MethodInfo methodInfo, TypeInfo typeInfo) { return (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(typeInfo) && typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == methodInfo); } private bool IsSilentRouteAttribute(IRouteTemplateProvider routeTemplateProvider) { return routeTemplateProvider.Template == null && routeTemplateProvider.Order == null && routeTemplateProvider.Name == null; } private static void AddRange(IList list, IEnumerable items) { foreach (var item in items) { list.Add(item); } } } }