// 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.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { internal class DefaultApplicationModelProvider : IApplicationModelProvider { private readonly MvcOptions _mvcOptions; private readonly IModelMetadataProvider _modelMetadataProvider; private readonly Func _supportsAllRequests; private readonly Func _supportsNonGetRequests; public DefaultApplicationModelProvider( IOptions mvcOptionsAccessor, IModelMetadataProvider modelMetadataProvider) { _mvcOptions = mvcOptionsAccessor.Value; _modelMetadataProvider = modelMetadataProvider; _supportsAllRequests = _ => true; _supportsNonGetRequests = context => !string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase); } /// public int Order => -1000; /// public void OnProvidersExecuting(ApplicationModelProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } foreach (var filter in _mvcOptions.Filters) { context.Result.Filters.Add(filter); } foreach (var controllerType in context.ControllerTypes) { var controllerModel = CreateControllerModel(controllerType); if (controllerModel == null) { continue; } 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 actionModel = CreateActionModel(controllerType, methodInfo); if (actionModel == null) { continue; } 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 void OnProvidersExecuted(ApplicationModelProviderContext context) { // Intentionally empty. } /// /// Creates a for the given . /// /// The . /// A for the given . internal ControllerModel CreateControllerModel(TypeInfo typeInfo) { if (typeInfo == null) { throw new ArgumentNullException(nameof(typeInfo)); } // For attribute routes on a controller, we 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; 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); // 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.Selectors, CreateSelectors(attributes)); controllerModel.ControllerName = typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? typeInfo.Name.Substring(0, typeInfo.Name.Length - "Controller".Length) : typeInfo.Name; AddRange(controllerModel.Filters, attributes.OfType()); foreach (var routeValueProvider in attributes.OfType()) { controllerModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue); } 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 . internal PropertyModel CreatePropertyModel(PropertyInfo propertyInfo) { if (propertyInfo == null) { throw new ArgumentNullException(nameof(propertyInfo)); } var attributes = propertyInfo.GetCustomAttributes(inherit: true); // BindingInfo for properties can be either specified by decorating the property with binding specific attributes. // ModelMetadata also adds information from the property's type and any configured IBindingMetadataProvider. var modelMetadata = _modelMetadataProvider.GetMetadataForProperty(propertyInfo.DeclaringType, propertyInfo.Name); var bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata); if (bindingInfo == null) { // Look for BindPropertiesAttribute on the handler type if no BindingInfo was inferred for the property. // This allows a user to enable model binding on properties by decorating the controller type with BindPropertiesAttribute. var declaringType = propertyInfo.DeclaringType; var bindPropertiesAttribute = declaringType.GetCustomAttribute(inherit: true); if (bindPropertiesAttribute != null) { var requestPredicate = bindPropertiesAttribute.SupportsGet ? _supportsAllRequests : _supportsNonGetRequests; bindingInfo = new BindingInfo { RequestPredicate = requestPredicate, }; } } var propertyModel = new PropertyModel(propertyInfo, attributes) { PropertyName = propertyInfo.Name, BindingInfo = bindingInfo, }; return propertyModel; } /// /// Creates the instance for the given action . /// /// The controller . /// The action . /// /// An instance for the given action or /// null if the does not represent an action. /// internal ActionModel CreateActionModel( TypeInfo typeInfo, MethodInfo methodInfo) { if (typeInfo == null) { throw new ArgumentNullException(nameof(typeInfo)); } if (methodInfo == null) { throw new ArgumentNullException(nameof(methodInfo)); } if (!IsAction(typeInfo, methodInfo)) { return null; } // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType // is needed to so that the result of ToArray() is object var attributes = methodInfo.GetCustomAttributes(inherit: true); var actionModel = new ActionModel(methodInfo, attributes); 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; } foreach (var routeValueProvider in attributes.OfType()) { actionModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue); } // Now we need to determine the action selection info (cross-section of routes and constraints) // // For attribute routes on a action, we 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; 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; } // 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); AddRange(actionModel.Selectors, CreateSelectors(applicableAttributes)); return actionModel; } /// /// 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. /// internal bool IsAction(TypeInfo typeInfo, MethodInfo methodInfo) { if (typeInfo == null) { throw new ArgumentNullException(nameof(typeInfo)); } if (methodInfo == null) { throw new ArgumentNullException(nameof(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; } // Overridden 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)) { 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 a for the given . /// /// The . /// A for the given . internal ParameterModel CreateParameterModel(ParameterInfo parameterInfo) { if (parameterInfo == null) { throw new ArgumentNullException(nameof(parameterInfo)); } var attributes = parameterInfo.GetCustomAttributes(inherit: true); BindingInfo bindingInfo; if (_mvcOptions.AllowValidatingTopLevelNodes && _modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase) { var modelMetadata = modelMetadataProviderBase.GetMetadataForParameter(parameterInfo); bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata); } else { // GetMetadataForParameter should only be used if the user has opted in to the 2.1 behavior. bindingInfo = BindingInfo.GetBindingInfo(attributes); } var parameterModel = new ParameterModel(parameterInfo, attributes) { ParameterName = parameterInfo.Name, BindingInfo = bindingInfo, }; return parameterModel; } private IList CreateSelectors(IList attributes) { // Route attributes create multiple selector models, we want to split the set of // attributes based on these so each selector 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 a selector for each attribute that 'defines' a route, and a single selector // 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 selectors: // 1. [HttpPost("Api/Things")] // 2. [HttpGet], [AcceptVerbs("POST", "PUT")] // // Another example of this situation is: // // [Route("api/Products")] // [AcceptVerbs("GET", "HEAD")] // [HttpPost("api/Products/new")] // // This will generate 2 selectors: // 1. [AcceptVerbs("GET", "HEAD")] // 2. [HttpPost] // // 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 createSelectorForSilentRouteProviders = false; foreach (var attribute in attributes) { if (attribute is IRouteTemplateProvider routeTemplateProvider) { if (IsSilentRouteAttribute(routeTemplateProvider)) { createSelectorForSilentRouteProviders = true; } else { routeProviders.Add(routeTemplateProvider); } } } 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 selectors. And... // // [HttpGet] // [Route("Products")] // public void Foo() { } // // Is one selector. if (!(routeProvider is IActionHttpMethodProvider)) { createSelectorForSilentRouteProviders = false; } } var selectorModels = new List(); if (routeProviders.Count == 0 && !createSelectorForSilentRouteProviders) { // Simple case, all attributes apply selectorModels.Add(CreateSelectorModel(route: null, attributes: 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 (ReferenceEquals(attribute, routeProvider)) { filteredAttributes.Add(attribute); } else if (InRouteProviders(routeProviders, attribute)) { // Exclude other route template providers // Example: // [HttpGet("template")] // [Route("template/{id}")] } else if ( routeProvider is IActionHttpMethodProvider && attribute is IActionHttpMethodProvider) { // Example: // [HttpGet("template")] // [AcceptVerbs("GET", "POST")] // // Exclude other http method providers if this route is an // http method provider. } else { filteredAttributes.Add(attribute); } } selectorModels.Add(CreateSelectorModel(routeProvider, filteredAttributes)); } if (createSelectorForSilentRouteProviders) { var filteredAttributes = new List(); foreach (var attribute in attributes) { if (!InRouteProviders(routeProviders, attribute)) { filteredAttributes.Add(attribute); } } selectorModels.Add(CreateSelectorModel(route: null, attributes: filteredAttributes)); } } return selectorModels; } private static bool InRouteProviders(List routeProviders, object attribute) { foreach (var rp in routeProviders) { if (ReferenceEquals(rp, attribute)) { return true; } } return false; } private static SelectorModel CreateSelectorModel(IRouteTemplateProvider route, IList attributes) { var selectorModel = new SelectorModel(); if (route != null) { selectorModel.AttributeRouteModel = new AttributeRouteModel(route); } AddRange(selectorModel.ActionConstraints, attributes.OfType()); AddRange(selectorModel.EndpointMetadata, attributes); // Simple case, all HTTP method attributes apply var httpMethods = attributes .OfType() .SelectMany(a => a.HttpMethods) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (httpMethods.Length > 0) { selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods)); } return selectorModel; } private bool IsIDisposableMethod(MethodInfo methodInfo) { // Ideally we do not want Dispose method to be exposed as an action. However there are some scenarios where a user // might want to expose a method with name "Dispose" (even though they might not be really disposing resources) // Example: A controller deriving from MVC's Controller type might wish to have a method with name Dispose, // in which case they can use the "new" keyword to hide the base controller's declaration. // Find where the method was originally declared var baseMethodInfo = methodInfo.GetBaseDefinition(); var declaringTypeInfo = baseMethodInfo.DeclaringType.GetTypeInfo(); return (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(declaringTypeInfo) && declaringTypeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == baseMethodInfo); } 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); } } } }