// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Text; using System.Threading; using System.Web.Http.Internal; using System.Web.Http.Properties; namespace System.Web.Http.Controllers { /// /// Reflection based action selector. /// We optimize for the case where we have an instance per /// instance but can support cases where there are many instances for one /// as well. In the latter case the lookup is slightly slower because it goes through /// the dictionary. /// public class ApiControllerActionSelector : IHttpActionSelector { private const string ActionRouteKey = "action"; private const string ControllerRouteKey = "controller"; private ActionSelectorCacheItem _fastCache; private readonly object _cacheKey = new object(); public virtual HttpActionDescriptor SelectAction(HttpControllerContext controllerContext) { if (controllerContext == null) { throw Error.ArgumentNull("controllerContext"); } ActionSelectorCacheItem internalSelector = GetInternalSelector(controllerContext.ControllerDescriptor); return internalSelector.SelectAction(controllerContext); } public virtual ILookup GetActionMapping(HttpControllerDescriptor controllerDescriptor) { if (controllerDescriptor == null) { throw Error.ArgumentNull("controllerDescriptor"); } ActionSelectorCacheItem internalSelector = GetInternalSelector(controllerDescriptor); return internalSelector.GetActionMapping(); } private ActionSelectorCacheItem GetInternalSelector(HttpControllerDescriptor controllerDescriptor) { // First check in the local fast cache and if not a match then look in the broader // HttpControllerDescriptor.Properties cache if (_fastCache == null) { ActionSelectorCacheItem selector = new ActionSelectorCacheItem(controllerDescriptor); Interlocked.CompareExchange(ref _fastCache, selector, null); return selector; } else if (_fastCache.HttpControllerDescriptor == controllerDescriptor) { // If the key matches and we already have the delegate for creating an instance then just execute it return _fastCache; } else { // If the key doesn't match then lookup/create delegate in the HttpControllerDescriptor.Properties for // that HttpControllerDescriptor instance ActionSelectorCacheItem selector = (ActionSelectorCacheItem)controllerDescriptor.Properties.GetOrAdd( _cacheKey, _ => new ActionSelectorCacheItem(controllerDescriptor)); return selector; } } // All caching is in a dedicated cache class, which may be optionally shared across selector instances. // Make this a private nested class so that nobody else can conflict with our state. // Cache is initialized during ctor on a single thread. private class ActionSelectorCacheItem { private readonly HttpControllerDescriptor _controllerDescriptor; private readonly ReflectedHttpActionDescriptor[] _actionDescriptors; private readonly IDictionary> _actionParameterNames = new Dictionary>(); private readonly ILookup _actionNameMapping; // Selection commonly looks up an action by verb. // Cache this mapping. These caches are completely optional and we still behave correctly if we cache miss. // We can adjust the specific set we cache based on profiler information. // Conceptually, this set of caches could be a HttpMethod --> ReflectedHttpActionDescriptor[]. // - Beware that HttpMethod has a very slow hash function (it does case-insensitive string hashing). So don't use Dict. // - there are unbounded number of http methods, so make sure the cache doesn't grow indefinitely. // - we can build the cache at startup and don't need to continually add to it. private readonly HttpMethod[] _cacheListVerbKinds = new HttpMethod[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Post }; private readonly ReflectedHttpActionDescriptor[][] _cacheListVerbs; public ActionSelectorCacheItem(HttpControllerDescriptor controllerDescriptor) { Contract.Assert(controllerDescriptor != null); // Initialize the cache entirely in the ctor on a single thread. _controllerDescriptor = controllerDescriptor; MethodInfo[] allMethods = _controllerDescriptor.ControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public); MethodInfo[] validMethods = Array.FindAll(allMethods, IsValidActionMethod); _actionDescriptors = new ReflectedHttpActionDescriptor[validMethods.Length]; for (int i = 0; i < validMethods.Length; i++) { MethodInfo method = validMethods[i]; ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(_controllerDescriptor, method); _actionDescriptors[i] = actionDescriptor; HttpActionBinding actionBinding = controllerDescriptor.ActionValueBinder.GetBinding(actionDescriptor); // Build action parameter name mapping, only consider parameters that are simple types, do not have default values and come from URI _actionParameterNames.Add( actionDescriptor, actionBinding.ParameterBindings .Where(binding => TypeHelper.IsSimpleUnderlyingType(binding.Descriptor.ParameterType) && !binding.HasDefaultValue() && binding.WillReadUri()) .Select(binding => binding.Descriptor.Prefix ?? binding.Descriptor.ParameterName)); } _actionNameMapping = _actionDescriptors.ToLookup(actionDesc => actionDesc.ActionName, StringComparer.OrdinalIgnoreCase); // Bucket the action descriptors by common verbs. int len = _cacheListVerbKinds.Length; _cacheListVerbs = new ReflectedHttpActionDescriptor[len][]; for (int i = 0; i < len; i++) { _cacheListVerbs[i] = FindActionsForVerbWorker(_cacheListVerbKinds[i]); } } public HttpControllerDescriptor HttpControllerDescriptor { get { return _controllerDescriptor; } } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing of response instance.")] public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext) { string actionName; bool useActionName = controllerContext.RouteData.Values.TryGetValue(ActionRouteKey, out actionName); ReflectedHttpActionDescriptor[] actionsFoundByHttpMethods; HttpMethod incomingMethod = controllerContext.Request.Method; // First get an initial candidate list. if (useActionName) { // We have an explicit {action} value, do traditional binding. Just lookup by actionName ReflectedHttpActionDescriptor[] actionsFoundByName = _actionNameMapping[actionName].ToArray(); // Throws HttpResponseException with NotFound status because no action matches the Name if (actionsFoundByName.Length == 0) { throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.NotFound, Error.Format(SRResources.ApiControllerActionSelector_ActionNameNotFound, _controllerDescriptor.ControllerName, actionName))); } // This filters out any incompatible verbs from the incoming action list actionsFoundByHttpMethods = actionsFoundByName.Where(actionDescriptor => actionDescriptor.SupportedHttpMethods.Contains(incomingMethod)).ToArray(); } else { // No {action} parameter, infer it from the verb. actionsFoundByHttpMethods = FindActionsForVerb(incomingMethod); } // Throws HttpResponseException with MethodNotAllowed status because no action matches the Http Method if (actionsFoundByHttpMethods.Length == 0) { throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.MethodNotAllowed, Error.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, incomingMethod))); } // If there are multiple candidates, then apply overload resolution logic. if (actionsFoundByHttpMethods.Length > 1) { actionsFoundByHttpMethods = FindActionUsingRouteAndQueryParameters(controllerContext, actionsFoundByHttpMethods).ToArray(); } List selectedActions = RunSelectionFilters(controllerContext, actionsFoundByHttpMethods); actionsFoundByHttpMethods = null; switch (selectedActions.Count) { case 0: // Throws HttpResponseException with NotFound status because no action matches the request throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.NotFound, Error.Format(SRResources.ApiControllerActionSelector_ActionNotFound, _controllerDescriptor.ControllerName))); case 1: return selectedActions[0]; default: // Throws HttpResponseException with InternalServerError status because multiple action matches the request string ambiguityList = CreateAmbiguousMatchList(selectedActions); throw new HttpResponseException(controllerContext.Request.CreateResponse( HttpStatusCode.InternalServerError, Error.Format(SRResources.ApiControllerActionSelector_AmbiguousMatch, ambiguityList))); } } public ILookup GetActionMapping() { return new LookupAdapter() { Source = _actionNameMapping }; } private IEnumerable FindActionUsingRouteAndQueryParameters(HttpControllerContext controllerContext, IEnumerable actionsFound) { // TODO, DevDiv 320655, improve performance of this method. IDictionary routeValues = controllerContext.RouteData.Values; IEnumerable routeParameterNames = routeValues.Select(route => route.Key) .Where(key => !String.Equals(key, ControllerRouteKey, StringComparison.OrdinalIgnoreCase) && !String.Equals(key, ActionRouteKey, StringComparison.OrdinalIgnoreCase)); IEnumerable queryParameterNames = controllerContext.Request.RequestUri.ParseQueryString().AllKeys; bool hasRouteParameters = routeParameterNames.Any(); bool hasQueryParameters = queryParameterNames.Any(); if (hasRouteParameters || hasQueryParameters) { // refine the results based on route parameters to make sure that route parameters take precedence over query parameters if (hasRouteParameters && hasQueryParameters) { // route parameters is a subset of action parameters actionsFound = actionsFound.Where(descriptor => !routeParameterNames.Except(_actionParameterNames[descriptor], StringComparer.OrdinalIgnoreCase).Any()); } // further refine the results making sure that action parameters is a subset of route parameters and query parameters if (actionsFound.Count() > 1) { IEnumerable combinedParameterNames = queryParameterNames.Union(routeParameterNames); // action parameters is a subset of route parameters and query parameters actionsFound = actionsFound.Where(descriptor => !_actionParameterNames[descriptor].Except(combinedParameterNames, StringComparer.OrdinalIgnoreCase).Any()); // select the results with the longest parameter match if (actionsFound.Count() > 1) { actionsFound = actionsFound .GroupBy(descriptor => _actionParameterNames[descriptor].Count()) .OrderByDescending(g => g.Key) .First(); } } } else { // return actions with no parameters actionsFound = actionsFound.Where(descriptor => !_actionParameterNames[descriptor].Any()); } return actionsFound; } private static List RunSelectionFilters(HttpControllerContext controllerContext, IEnumerable descriptorsFound) { // remove all methods which are opting out of this request // to opt out, at least one attribute defined on the method must return false List matchesWithSelectionAttributes = null; List matchesWithoutSelectionAttributes = new List(); foreach (ReflectedHttpActionDescriptor actionDescriptor in descriptorsFound) { IActionMethodSelector[] attrs = actionDescriptor.CacheAttrsIActionMethodSelector; if (attrs.Length == 0) { matchesWithoutSelectionAttributes.Add(actionDescriptor); } else { bool match = Array.TrueForAll(attrs, selector => selector.IsValidForRequest(controllerContext, actionDescriptor.MethodInfo)); if (match) { if (matchesWithSelectionAttributes == null) { matchesWithSelectionAttributes = new List(); } matchesWithSelectionAttributes.Add(actionDescriptor); } } } // if a matching action method had a selection attribute, consider it more specific than a matching action method // without a selection attribute if ((matchesWithSelectionAttributes != null) && (matchesWithSelectionAttributes.Count > 0)) { return matchesWithSelectionAttributes; } else { return matchesWithoutSelectionAttributes; } } // This is called when we don't specify an Action name // Get list of actions that match a given verb. This can match by name or IActionHttpMethodSelecto private ReflectedHttpActionDescriptor[] FindActionsForVerb(HttpMethod verb) { // Check cache for common verbs. for (int i = 0; i < _cacheListVerbKinds.Length; i++) { // verb selection on common verbs is normalized to have object reference identity. // This is significantly more efficient than comparing the verbs based on strings. if (Object.ReferenceEquals(verb, _cacheListVerbKinds[i])) { return _cacheListVerbs[i]; } } // General case for any verbs. return FindActionsForVerbWorker(verb); } // This is called when we don't specify an Action name // Get list of actions that match a given verb. This can match by name or IActionHttpMethodSelector. // Since this list is fixed for a given verb type, it can be pre-computed and cached. // This function should not do caching. It's the helper that builds the caches. private ReflectedHttpActionDescriptor[] FindActionsForVerbWorker(HttpMethod verb) { List listMethods = new List(); foreach (ReflectedHttpActionDescriptor descriptor in _actionDescriptors) { if (descriptor.SupportedHttpMethods.Contains(verb)) { listMethods.Add(descriptor); } } return listMethods.ToArray(); } private static string CreateAmbiguousMatchList(IEnumerable ambiguousDescriptors) { StringBuilder exceptionMessageBuilder = new StringBuilder(); foreach (ReflectedHttpActionDescriptor descriptor in ambiguousDescriptors) { MethodInfo methodInfo = descriptor.MethodInfo; exceptionMessageBuilder.AppendLine(); exceptionMessageBuilder.Append(Error.Format( SRResources.ActionSelector_AmbiguousMatchType, methodInfo, methodInfo.DeclaringType.FullName)); } return exceptionMessageBuilder.ToString(); } private static bool IsValidActionMethod(MethodInfo methodInfo) { if (methodInfo.IsSpecialName) { // not a normal method, e.g. a constructor or an event return false; } if (methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(TypeHelper.ApiControllerType)) { // is a method on Object, IHttpController, ApiController return false; } return true; } } // We need to expose ILookup, but we have a ILookup // ReflectedHttpActionDescriptor derives from HttpActionDescriptor, but ILookup doesn't support Covariance. // Adapter class since ILookup doesn't support Covariance. // Fortunately, IGrouping, IEnumerable support Covariance, so it's easy to forward. private class LookupAdapter : ILookup { public ILookup Source; public int Count { get { return Source.Count; } } public IEnumerable this[string key] { get { return Source[key]; } } public bool Contains(string key) { return Source.Contains(key); } public IEnumerator> GetEnumerator() { return Source.GetEnumerator(); } Collections.IEnumerator Collections.IEnumerable.GetEnumerator() { return Source.GetEnumerator(); } } } }