// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Linq.Expressions; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Web.Http.Filters; using System.Web.Http.Internal; using System.Web.Http.Properties; namespace System.Web.Http.Controllers { /// /// An action descriptor representing a reflected synchronous or asynchronous action method. /// public class ReflectedHttpActionDescriptor : HttpActionDescriptor { private static readonly object[] _empty = new object[0]; private readonly Lazy> _parameters; private Lazy _actionExecutor; private MethodInfo _methodInfo; private Type _returnType; private string _actionName; private Collection _supportedHttpMethods; // Getting custom attributes via reflection is slow. // But iterating over a object[] to pick out specific types is fast. // Furthermore, many different services may call to ask for different attributes, so we have multiple callers. // That means there's not a single cache for the callers, which means there's some value caching here. // This cache can be a 2x speedup in some benchmarks. private object[] _attrCached; private static readonly HttpMethod[] _supportedHttpMethodsByConvention = { HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Head, HttpMethod.Options, new HttpMethod("PATCH") }; /// /// Initializes a new instance of the class. /// /// The default constructor is intended for use by unit testing only. public ReflectedHttpActionDescriptor() { _parameters = new Lazy>(() => InitializeParameterDescriptors()); _supportedHttpMethods = new Collection(); } public ReflectedHttpActionDescriptor(HttpControllerDescriptor controllerDescriptor, MethodInfo methodInfo) : base(controllerDescriptor) { if (methodInfo == null) { throw Error.ArgumentNull("methodInfo"); } InitializeProperties(methodInfo); _parameters = new Lazy>(() => InitializeParameterDescriptors()); } /// /// Caches that the ActionSelector use. /// internal IActionMethodSelector[] CacheAttrsIActionMethodSelector { get; private set; } public override string ActionName { get { return _actionName; } } public override Collection SupportedHttpMethods { get { return _supportedHttpMethods; } } public MethodInfo MethodInfo { get { return _methodInfo; } set { if (value == null) { throw Error.PropertyNull(); } InitializeProperties(value); } } /// /// The return type of the method or null if the method does not return a value (e.g. a method returning /// void). /// /// /// This implementation returns the exact value of for /// synchronous methods and an unwrapped value for asynchronous methods (e.g. the T of . /// This returns null for methods returning void or . /// public override Type ReturnType { get { return _returnType; } } public override Collection GetCustomAttributes() { Contract.Assert(_methodInfo != null); // can't get attributes without the method set! Contract.Assert(_attrCached != null); // setting the method should build the attribute cache return new Collection(TypeHelper.OfType(_attrCached)); } /// /// Executes the described action and returns a that once completed will /// contain the return value of the action. /// /// The context. /// The arguments. /// A that once completed will contain the return value of the action. public override Task ExecuteAsync(HttpControllerContext controllerContext, IDictionary arguments) { if (controllerContext == null) { throw Error.ArgumentNull("controllerContext"); } if (arguments == null) { throw Error.ArgumentNull("arguments"); } return TaskHelpers.RunSynchronously(() => { object[] argumentValues = PrepareParameters(arguments, controllerContext); return _actionExecutor.Value.Execute(controllerContext.Controller, argumentValues); }); } public override Collection GetFilters() { return new Collection(GetCustomAttributes().Concat(base.GetFilters()).ToList()); } public override Collection GetParameters() { return _parameters.Value; } private void InitializeProperties(MethodInfo methodInfo) { _methodInfo = methodInfo; _returnType = GetReturnType(methodInfo); _actionExecutor = new Lazy(() => InitializeActionExecutor(_methodInfo)); _attrCached = _methodInfo.GetCustomAttributes(inherit: true); CacheAttrsIActionMethodSelector = _attrCached.OfType().ToArray(); _actionName = GetActionName(_methodInfo, _attrCached); _supportedHttpMethods = GetSupportedHttpMethods(_methodInfo, _attrCached); } internal static Type GetReturnType(MethodInfo methodInfo) { Type result = methodInfo.ReturnType; if (typeof(Task).IsAssignableFrom(result)) { result = TypeHelper.GetTaskInnerTypeOrNull(methodInfo.ReturnType); } if (result == typeof(void)) { result = null; } return result; } private Collection InitializeParameterDescriptors() { Contract.Assert(_methodInfo != null); List parameterInfos = _methodInfo.GetParameters().Select( (item) => new ReflectedHttpParameterDescriptor(this, item)).ToList(); return new Collection(parameterInfos); } private object[] PrepareParameters(IDictionary parameters, HttpControllerContext controllerContext) { // This is on a hotpath, so a quick check to avoid the allocation if we have no parameters. if (_parameters.Value.Count == 0) { return _empty; } ParameterInfo[] parameterInfos = MethodInfo.GetParameters(); var rawParameterValues = from parameterInfo in parameterInfos select ExtractParameterFromDictionary(parameterInfo, parameters, controllerContext); object[] parametersArray = rawParameterValues.ToArray(); return parametersArray; } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing of response instance.")] private object ExtractParameterFromDictionary(ParameterInfo parameterInfo, IDictionary parameters, HttpControllerContext controllerContext) { object value; if (!parameters.TryGetValue(parameterInfo.Name, out value)) { // the key should always be present, even if the parameter value is null throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.BadRequest, Error.Format(SRResources.ReflectedActionDescriptor_ParameterNotInDictionary, parameterInfo.Name, parameterInfo.ParameterType, MethodInfo, MethodInfo.DeclaringType))); } if (value == null && !TypeHelper.TypeAllowsNullValue(parameterInfo.ParameterType)) { // tried to pass a null value for a non-nullable parameter type throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.BadRequest, Error.Format(SRResources.ReflectedActionDescriptor_ParameterCannotBeNull, parameterInfo.Name, parameterInfo.ParameterType, MethodInfo, MethodInfo.DeclaringType))); } if (value != null && !parameterInfo.ParameterType.IsInstanceOfType(value)) { // value was supplied but is not of the proper type throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.BadRequest, Error.Format(SRResources.ReflectedActionDescriptor_ParameterValueHasWrongType, parameterInfo.Name, MethodInfo, MethodInfo.DeclaringType, value.GetType(), parameterInfo.ParameterType))); } return value; } private static string GetActionName(MethodInfo methodInfo, object[] actionAttributes) { ActionNameAttribute nameAttribute = TypeHelper.OfType(actionAttributes).FirstOrDefault(); return nameAttribute != null ? nameAttribute.Name : methodInfo.Name; } private static Collection GetSupportedHttpMethods(MethodInfo methodInfo, object[] actionAttributes) { Collection supportedHttpMethods = new Collection(); ICollection httpMethodProviders = TypeHelper.OfType(actionAttributes); if (httpMethodProviders.Count > 0) { // Get HttpMethod from attributes foreach (IActionHttpMethodProvider httpMethodSelector in httpMethodProviders) { foreach (HttpMethod httpMethod in httpMethodSelector.HttpMethods) { supportedHttpMethods.Add(httpMethod); } } } else { // Get HttpMethod from method name convention for (int i = 0; i < _supportedHttpMethodsByConvention.Length; i++) { if (methodInfo.Name.StartsWith(_supportedHttpMethodsByConvention[i].Method, StringComparison.OrdinalIgnoreCase)) { supportedHttpMethods.Add(_supportedHttpMethodsByConvention[i]); break; } } } if (supportedHttpMethods.Count == 0) { // Use POST as the default HttpMethod supportedHttpMethods.Add(HttpMethod.Post); } return supportedHttpMethods; } private static ActionExecutor InitializeActionExecutor(MethodInfo methodInfo) { if (methodInfo.ContainsGenericParameters) { throw Error.InvalidOperation(SRResources.ReflectedHttpActionDescriptor_CannotCallOpenGenericMethods, methodInfo, methodInfo.ReflectedType.FullName); } return new ActionExecutor(methodInfo); } private sealed class ActionExecutor { private static readonly Task _completedTaskReturningNull = TaskHelpers.FromResult(null); private readonly Func> _executor; private static MethodInfo _convertOfTMethod = typeof(ActionExecutor).GetMethod("Convert", BindingFlags.Static | BindingFlags.NonPublic); public ActionExecutor(MethodInfo methodInfo) { Contract.Assert(methodInfo != null); _executor = GetExecutor(methodInfo); } public Task Execute(object instance, object[] arguments) { return _executor(instance, arguments); } // Method called via reflection. private static Task Convert(object taskAsObject) { Task task = (Task)taskAsObject; return task.Then(r => (object)r); } // Do not inline or optimize this method to avoid stack-related reflection demand issues when // running from the GAC in medium trust [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] private static Func> CompileGenericTaskConversionDelegate(Type taskValueType) { Contract.Assert(taskValueType != null); return (Func>)Delegate.CreateDelegate(typeof(Func>), _convertOfTMethod.MakeGenericMethod(taskValueType)); } private static Func> GetExecutor(MethodInfo methodInfo) { // Parameters to executor ParameterExpression instanceParameter = Expression.Parameter(typeof(object), "instance"); ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); // Build parameter list List parameters = new List(); ParameterInfo[] paramInfos = methodInfo.GetParameters(); for (int i = 0; i < paramInfos.Length; i++) { ParameterInfo paramInfo = paramInfos[i]; BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); // valueCast is "(Ti) parameters[i]" parameters.Add(valueCast); } // Call method UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(instanceParameter, methodInfo.ReflectedType) : null; MethodCallExpression methodCall = methodCall = Expression.Call(instanceCast, methodInfo, parameters); // methodCall is "((MethodInstanceType) instance).method((T0) parameters[0], (T1) parameters[1], ...)" // Create function if (methodCall.Type == typeof(void)) { // for: public void Action() Expression> lambda = Expression.Lambda>(methodCall, instanceParameter, parametersParameter); Action voidExecutor = lambda.Compile(); return (instance, methodParameters) => { voidExecutor(instance, methodParameters); return _completedTaskReturningNull; }; } else { // must coerce methodCall to match Func signature UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object)); Expression> lambda = Expression.Lambda>(castMethodCall, instanceParameter, parametersParameter); Func compiled = lambda.Compile(); if (methodCall.Type == typeof(Task)) { // for: public Task Action() return (instance, methodParameters) => { Task r = (Task)compiled(instance, methodParameters); ThrowIfWrappedTaskInstance(methodInfo, r.GetType()); return r.Then(() => (object)null); }; } else if (typeof(Task).IsAssignableFrom(methodCall.Type)) { // for: public Task Action() // constructs: return (Task)Convert(((Task)instance).method((T0) param[0], ...)) Type taskValueType = TypeHelper.GetTaskInnerTypeOrNull(methodCall.Type); var compiledConversion = CompileGenericTaskConversionDelegate(taskValueType); return (instance, methodParameters) => { object callResult = compiled(instance, methodParameters); Task convertedResult = compiledConversion(callResult); return convertedResult; }; } else { // for: public T Action() return (instance, methodParameters) => { var result = compiled(instance, methodParameters); // Throw when the result of a method is Task. Asynchronous methods need to declare that they // return a Task. Task resultAsTask = result as Task; if (resultAsTask != null) { throw Error.InvalidOperation(SRResources.ActionExecutor_UnexpectedTaskInstance, methodInfo.Name, methodInfo.DeclaringType.Name); } return TaskHelpers.FromResult(result); }; } } } private static void ThrowIfWrappedTaskInstance(MethodInfo method, Type type) { // Throw if a method declares a return type of Task and returns an instance of Task or Task> // This most likely indicates that the developer forgot to call Unwrap() somewhere. Contract.Assert(method.ReturnType == typeof(Task)); // Fast path: check if type is exactly Task first. if (type != typeof(Task)) { Type innerTaskType = TypeHelper.GetTaskInnerTypeOrNull(type); if (innerTaskType != null && typeof(Task).IsAssignableFrom(innerTaskType)) { throw Error.InvalidOperation(SRResources.ActionExecutor_WrappedTaskInstance, method.Name, method.DeclaringType.Name, type.FullName); } } } } } }