// 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.Mvc.Description; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; namespace Microsoft.AspNet.Mvc.ApplicationModels { /// /// A default implementation of . /// public class DefaultActionModelBuilder : IActionModelBuilder { /// 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 from action for each attribute that 'defines' a route, and a single action // for all of the ones that don't (if any exist). // // Ex: // [HttpGet] // [AcceptVerbs("POST", "PUT")] // [Route("Api/Things")] // public void DoThing() // // This will generate 2 actions: // 1. [Route("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 splitAttributes = new List(); var hasSilentRouteAttribute = false; foreach (var attribute in attributes) { var routeTemplateProvider = attribute as IRouteTemplateProvider; if (routeTemplateProvider != null) { if (IsSilentRouteAttribute(routeTemplateProvider)) { hasSilentRouteAttribute = true; } else { splitAttributes.Add(attribute); } } } var actionModels = new List(); if (splitAttributes.Count == 0 && !hasSilentRouteAttribute) { actionModels.Add(CreateActionModel(methodInfo, attributes)); } else { foreach (var splitAttribute in splitAttributes) { var filteredAttributes = new List(); foreach (var attribute in attributes) { if (attribute == splitAttribute) { filteredAttributes.Add(attribute); } else if (attribute is IRouteTemplateProvider) { // Exclude other route template providers } else { filteredAttributes.Add(attribute); } } actionModels.Add(CreateActionModel(methodInfo, filteredAttributes)); } if (hasSilentRouteAttribute) { var filteredAttributes = new List(); foreach (var attribute in attributes) { if (!splitAttributes.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) { IsActionNameMatchRequired = true, }; actionModel.ActionConstraints.AddRange(attributes.OfType()); actionModel.Filters.AddRange(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(); actionModel.HttpMethods.AddRange( httpMethods .Where(a => a.HttpMethods != null) .SelectMany(a => a.HttpMethods) .Distinct()); var routeTemplateProvider = attributes.OfType().FirstOrDefault(); if (routeTemplateProvider != null && !IsSilentRouteAttribute(routeTemplateProvider)) { 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; parameterModel.IsOptional = parameterInfo.HasDefaultValue; return parameterModel; } private bool IsSilentRouteAttribute(IRouteTemplateProvider routeTemplateProvider) { return routeTemplateProvider.Template == null && routeTemplateProvider.Order == null && routeTemplateProvider.Name == null; } } }