// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http.Filters; using System.Web.Http.Internal; using System.Web.Http.Properties; namespace System.Web.Http.Controllers { public abstract class HttpActionDescriptor { private readonly ConcurrentDictionary _properties = new ConcurrentDictionary(); private IActionResultConverter _converter; private readonly Lazy> _filterPipeline; private HttpConfiguration _configuration; private HttpControllerDescriptor _controllerDescriptor; private readonly Collection _supportedHttpMethods = new Collection(); private HttpActionBinding _actionBinding; private static readonly ResponseMessageResultConverter _responseMessageResultConverter = new ResponseMessageResultConverter(); private static readonly VoidResultConverter _voidResultConverter = new VoidResultConverter(); protected HttpActionDescriptor() { _filterPipeline = new Lazy>(InitializeFilterPipeline); } protected HttpActionDescriptor(HttpControllerDescriptor controllerDescriptor) : this() { if (controllerDescriptor == null) { throw Error.ArgumentNull("controllerDesriptor"); } _controllerDescriptor = controllerDescriptor; _configuration = _controllerDescriptor.Configuration; } public abstract string ActionName { get; } public HttpConfiguration Configuration { get { return _configuration; } set { if (value == null) { throw Error.PropertyNull(); } _configuration = value; } } public virtual HttpActionBinding ActionBinding { get { if (_actionBinding == null) { IActionValueBinder actionValueBinder = _controllerDescriptor.ActionValueBinder; HttpActionBinding actionBinding = actionValueBinder.GetBinding(this); _actionBinding = actionBinding; } return _actionBinding; } set { if (value == null) { throw Error.PropertyNull(); } _actionBinding = value; } } public HttpControllerDescriptor ControllerDescriptor { get { return _controllerDescriptor; } set { if (value == null) { throw Error.PropertyNull(); } _controllerDescriptor = value; } } /// /// The return type of the method or null if the method does not return a value (e.g. a method returning /// void). /// /// /// This property should describe the type of the value contained by the result of executing the action /// via the . /// public abstract Type ReturnType { get; } /// /// Gets the converter for correctly transforming the result of calling /// into an instance of /// . /// /// /// The behavior of the returned converter should align with the action's declared . /// public virtual IActionResultConverter ResultConverter { get { // This initialization is not thread safe but that's fine since the converters do not have // any interesting state. If 2 threads get 2 different instances of the same converter type // we don't really care. if (_converter == null) { _converter = GetResultConverter(ReturnType); } return _converter; } } public virtual Collection SupportedHttpMethods { get { return _supportedHttpMethods; } } /// /// Gets the properties associated with this instance. /// public ConcurrentDictionary Properties { get { return _properties; } } public virtual Collection GetCustomAttributes() where T : class { return new Collection(); } [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Filters can be built dynamically")] public virtual Collection GetFilters() { return new Collection(); } [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Parameters can be built dynamically")] public abstract Collection GetParameters(); internal static IActionResultConverter GetResultConverter(Type type) { if (type != null && type.IsGenericParameter) { // This can happen if somebody declares an action method as: // public T Get() { } throw Error.InvalidOperation(SRResources.HttpActionDescriptor_NoConverterForGenericParamterTypeExists, type); } if (type == null) { return _voidResultConverter; } else if (typeof(HttpResponseMessage).IsAssignableFrom(type)) { return _responseMessageResultConverter; } else { Type valueConverterType = typeof(ValueResultConverter<>).MakeGenericType(type); return TypeActivator.Create(valueConverterType).Invoke(); } } /// /// 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 abstract Task ExecuteAsync(HttpControllerContext controllerContext, IDictionary arguments); /// /// Returns the filters for the given configuration and action. The filter collection is ordered /// according to the FilterScope (in order from least specific to most specific: First, Global, Controller, Action). /// /// If a given filter disallows duplicates (AllowMultiple=False) then the most specific filter is maintained /// and less specific filters get removed (e.g. if there is a Authorize filter with a Controller scope and another /// one with an Action scope then the one with the Action scope will be maintained and the one with the Controller /// scope will be discarded). /// /// A of all filters associated with this . [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Filter pipeline can be built dynamically")] public virtual Collection GetFilterPipeline() { return _filterPipeline.Value; } private Collection InitializeFilterPipeline() { IEnumerable filterProviders = _configuration.Services.GetFilterProviders(); IEnumerable filters = filterProviders.SelectMany(fp => fp.GetFilters(_configuration, this)).OrderBy(f => f, FilterInfoComparer.Instance); // Need to discard duplicate filters from the end, so that most specific ones get kept (Action scope) and // less specific ones get removed (Global) filters = RemoveDuplicates(filters.Reverse()).Reverse(); return new Collection(filters.ToList()); } private static IEnumerable RemoveDuplicates(IEnumerable filters) { Contract.Assert(filters != null); HashSet visitedTypes = new HashSet(); foreach (FilterInfo filter in filters) { object filterInstance = filter.Instance; Type filterInstanceType = filterInstance.GetType(); if (!visitedTypes.Contains(filterInstanceType) || AllowMultiple(filterInstance)) { yield return filter; visitedTypes.Add(filterInstanceType); } } } private static bool AllowMultiple(object filterInstance) { IFilter filter = filterInstance as IFilter; return filter == null || filter.AllowMultiple; } } }