// 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.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;
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 ParameterInfo[] _parameterInfos;
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[] _attributeCache;
private object[] _declaredOnlyAttributeCache;
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());
}
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);
}
}
private ParameterInfo[] ParameterInfos
{
get
{
if (_parameterInfos == null)
{
_parameterInfos = _methodInfo.GetParameters();
}
return _parameterInfos;
}
}
///
public override Type ReturnType
{
get { return _returnType; }
}
///
public override Collection GetCustomAttributes(bool inherit)
{
object[] attributes = inherit ? _attributeCache : _declaredOnlyAttributeCache;
return new Collection(TypeHelper.OfType(attributes));
}
///
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
public override Task ExecuteAsync(HttpControllerContext controllerContext, IDictionary arguments, CancellationToken cancellationToken)
{
if (controllerContext == null)
{
throw Error.ArgumentNull("controllerContext");
}
if (arguments == null)
{
throw Error.ArgumentNull("arguments");
}
if (cancellationToken.IsCancellationRequested)
{
return TaskHelpers.Canceled();
}
try
{
object[] argumentValues = PrepareParameters(arguments, controllerContext);
return _actionExecutor.Value.Execute(controllerContext.Controller, argumentValues);
}
catch (Exception e)
{
return TaskHelpers.FromError(e);
}
}
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;
_parameterInfos = null;
_returnType = GetReturnType(methodInfo);
_actionExecutor = new Lazy(() => InitializeActionExecutor(_methodInfo));
_declaredOnlyAttributeCache = _methodInfo.GetCustomAttributes(inherit: false);
_attributeCache = _methodInfo.GetCustomAttributes(inherit: true);
_actionName = GetActionName(_methodInfo, _attributeCache);
_supportedHttpMethods = GetSupportedHttpMethods(_methodInfo, _attributeCache);
}
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 = ParameterInfos.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 = ParameterInfos;
int parameterCount = parameterInfos.Length;
object[] parameterValues = new object[parameterCount];
for (int parameterIndex = 0; parameterIndex < parameterCount; parameterIndex++)
{
parameterValues[parameterIndex] = ExtractParameterFromDictionary(parameterInfos[parameterIndex], parameters, controllerContext);
}
return parameterValues;
}
[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.CreateErrorResponse(
HttpStatusCode.BadRequest,
SRResources.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.CreateErrorResponse(
HttpStatusCode.BadRequest,
SRResources.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.CreateErrorResponse(
HttpStatusCode.BadRequest,
SRResources.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;
}
// Implementing Equals and GetHashCode is needed here because when tracing is enabled, a different set of action descriptors
// are available at configuration time for attribute routing and at runtime. This is because the default action selector
// clears its action descriptor cache when the controller descriptor is different. And since tracing wraps the controller
// descriptor for tracing, the cache gets cleared and new action descriptors get created for tracing. We need to compare
// the action descriptors by method info to be able to correlate attribute routing actions to the tracing action descriptors.
///
public override int GetHashCode()
{
if (_methodInfo != null)
{
return _methodInfo.GetHashCode();
}
return base.GetHashCode();
}
///
public override bool Equals(object obj)
{
if (_methodInfo != null)
{
ReflectedHttpActionDescriptor otherDescriptor = obj as ReflectedHttpActionDescriptor;
if (otherDescriptor == null)
{
return false;
}
return _methodInfo.Equals(otherDescriptor._methodInfo);
}
return base.Equals(obj);
}
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 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.CastToObject();
}
// 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 = 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 TaskHelpers.NullResult();
};
}
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.CastToObject();
};
}
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 Task.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);
}
}
}
}
}
}