// 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);
}
}
}
}
}