// 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.Http; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Web.Http.Filters; using System.Web.Http.ModelBinding; using System.Web.Http.Properties; using System.Web.Http.Routing; namespace System.Web.Http { public abstract class ApiController : IHttpController, IDisposable { private bool _disposed; private HttpRequestMessage _request; private ModelStateDictionary _modelState; private HttpConfiguration _configuration; private HttpControllerContext _controllerContext; /// /// Gets the of the current ApiController. /// /// The setter is not intended to be used other than for unit testing purpose. /// public HttpRequestMessage Request { get { return _request; } set { if (value == null) { throw Error.ArgumentNull("value"); } _request = value; } } /// /// Gets the of the current ApiController. /// /// The setter is not intended to be used other than for unit testing purpose. /// public HttpConfiguration Configuration { get { return _configuration; } set { if (value == null) { throw Error.ArgumentNull("value"); } _configuration = value; } } /// /// Gets the of the current ApiController. /// /// The setter is not intended to be used other than for unit testing purpose. /// public HttpControllerContext ControllerContext { get { return _controllerContext; } set { if (value == null) { throw Error.ArgumentNull("value"); } _controllerContext = value; } } /// /// Gets model state after the model binding process. This ModelState will be empty before model binding happens. /// Please do not populate this property other than for unit testing purpose. /// public ModelStateDictionary ModelState { get { if (_modelState == null) { // The getter is not intended to be used by multiple threads, so it is fine to initialize here _modelState = new ModelStateDictionary(); } return _modelState; } } /// /// Returns an instance of a UrlHelper, which is used to generate URLs to other APIs. /// public UrlHelper Url { get { return ControllerContext.Url; } } /// /// Returns the current principal associated with this request. /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "That would make for poor usability.")] public IPrincipal User { get { return Thread.CurrentPrincipal; } } public virtual Task ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) { if (_request != null) { // if user has registered a controller factory which produces the same controller instance, we should throw here throw Error.InvalidOperation(SRResources.CannotSupportSingletonInstance, typeof(ApiController).Name, typeof(IHttpControllerActivator).Name); } Initialize(controllerContext); // We can't be reused, and we know we're disposable, so make sure we go away when // the request has been completed. if (_request != null) { _request.RegisterForDispose(this); } HttpControllerDescriptor controllerDescriptor = controllerContext.ControllerDescriptor; HttpActionDescriptor actionDescriptor = controllerDescriptor.HttpActionSelector.SelectAction(controllerContext); HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); IEnumerable filters = actionDescriptor.GetFilterPipeline(); FilterGrouping filterGrouping = new FilterGrouping(filters); IEnumerable actionFilters = filterGrouping.ActionFilters; IEnumerable authorizationFilters = filterGrouping.AuthorizationFilters; IEnumerable exceptionFilters = filterGrouping.ExceptionFilters; // Func> Task result = InvokeActionWithAuthorizationFilters(actionContext, cancellationToken, authorizationFilters, () => { HttpActionBinding actionBinding = actionDescriptor.ActionBinding; Task bindTask = actionBinding.ExecuteBindingAsync(actionContext, cancellationToken); return bindTask.Then(() => { _modelState = actionContext.ModelState; Func> invokeFunc = InvokeActionWithActionFilters(actionContext, cancellationToken, actionFilters, () => { return controllerDescriptor.HttpActionInvoker.InvokeActionAsync(actionContext, cancellationToken); }); return invokeFunc(); }); })(); result = InvokeActionWithExceptionFilters(result, actionContext, cancellationToken, exceptionFilters); return result; } protected virtual void Initialize(HttpControllerContext controllerContext) { if (controllerContext == null) { throw Error.ArgumentNull("controllerContext"); } ControllerContext = controllerContext; _request = controllerContext.Request; _configuration = controllerContext.Configuration; } internal static Task InvokeActionWithExceptionFilters(Task actionTask, HttpActionContext actionContext, CancellationToken cancellationToken, IEnumerable filters) { Contract.Assert(actionTask != null); Contract.Assert(actionContext != null); Contract.Assert(filters != null); return actionTask.Catch( info => { HttpActionExecutedContext executedContext = new HttpActionExecutedContext(actionContext, info.Exception); // Note: exception filters need to be scheduled in the reverse order so that // the more specific filter (e.g. Action) executes before the less specific ones (e.g. Global) filters = filters.Reverse(); // Note: in order to work correctly with the TaskHelpers.Iterate method, the lazyTaskEnumeration // must be lazily evaluated. Otherwise all the tasks might start executing even though we want to run them // sequentially and not invoke any of the following ones if an earlier fails. IEnumerable lazyTaskEnumeration = filters.Select(filter => filter.ExecuteExceptionFilterAsync(executedContext, cancellationToken)); Task resultTask = TaskHelpers.Iterate(lazyTaskEnumeration, cancellationToken) .Then(() => { if (executedContext.Response != null) { return TaskHelpers.FromResult(executedContext.Response); } else { return TaskHelpers.FromError(executedContext.Exception); } }); return info.Task(resultTask); }); } internal static Func> InvokeActionWithAuthorizationFilters(HttpActionContext actionContext, CancellationToken cancellationToken, IEnumerable filters, Func> innerAction) { Contract.Assert(actionContext != null); Contract.Assert(filters != null); Contract.Assert(innerAction != null); // Because the continuation gets built from the inside out we need to reverse the filter list // so that least specific filters (Global) get run first and the most specific filters (Action) get run last. filters = filters.Reverse(); Func> result = filters.Aggregate(innerAction, (continuation, filter) => { return () => filter.ExecuteAuthorizationFilterAsync(actionContext, cancellationToken, continuation); }); return result; } internal static Func> InvokeActionWithActionFilters(HttpActionContext actionContext, CancellationToken cancellationToken, IEnumerable filters, Func> innerAction) { Contract.Assert(actionContext != null); Contract.Assert(filters != null); Contract.Assert(innerAction != null); // Because the continuation gets built from the inside out we need to reverse the filter list // so that least specific filters (Global) get run first and the most specific filters (Action) get run last. filters = filters.Reverse(); Func> result = filters.Aggregate(innerAction, (continuation, filter) => { return () => filter.ExecuteActionFilterAsync(actionContext, cancellationToken, continuation); }); return result; } #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { _disposed = true; if (disposing) { // TODO: Dispose controller state } } } #endregion /// /// Quickly split filters into different types /// /// Avoid because it has a very slow implementation that shows on profiles. private class FilterGrouping { private List _actionFilters = new List(); private List _authorizationFilters = new List(); private List _exceptionFilters = new List(); public FilterGrouping(IEnumerable filters) { Contract.Assert(filters != null); foreach (FilterInfo f in filters) { var filter = f.Instance; Categorize(filter, _actionFilters); Categorize(filter, _authorizationFilters); Categorize(filter, _exceptionFilters); } } public IEnumerable ActionFilters { get { return _actionFilters; } } public IEnumerable AuthorizationFilters { get { return _authorizationFilters; } } public IEnumerable ExceptionFilters { get { return _exceptionFilters; } } private static void Categorize(IFilter filter, List list) where T : class { T match = filter as T; if (match != null) { list.Add(match); } } } } }