// 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; using System.Reflection; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Mvc.Description; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; using Microsoft.Framework.Internal; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Mvc.ApplicationModels { /// /// A default implementation of . /// public class DefaultActionModelBuilder : IActionModelBuilder { private readonly AuthorizationOptions _authorizationOptions; public DefaultActionModelBuilder(IOptions options) { _authorizationOptions = options?.Options ?? new AuthorizationOptions(); } /// public IEnumerable BuildActionModels([NotNull] TypeInfo typeInfo, [NotNull] MethodInfo methodInfo) { if (!IsAction(typeInfo, methodInfo)) { return Enumerable.Empty(); } // 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(); // 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)); } } foreach (var actionModel in actionModels) { foreach (var parameterInfo in actionModel.ActionMethod.GetParameters()) { var parameterModel = CreateParameterModel(parameterInfo); if (parameterModel != null) { parameterModel.Action = actionModel; actionModel.Parameters.Add(parameterModel); } } } 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; } private bool IsIDisposableMethod(MethodInfo methodInfo, TypeInfo typeInfo) { return (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(typeInfo) && typeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == methodInfo); } /// /// 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 policy = AuthorizationPolicy.Combine(_authorizationOptions, attributes.OfType()); if (policy != null) { actionModel.Filters.Add(new AuthorizeFilter(policy)); } 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); parameterModel.BinderMetadata = attributes.OfType().FirstOrDefault(); parameterModel.ParameterName = parameterInfo.Name; return parameterModel; } 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); } } } }