// 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.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Security.Principal; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Web.Http.ExceptionHandling; using System.Web.Http.Filters; using System.Web.Http.Metadata; using System.Web.Http.ModelBinding; using System.Web.Http.Properties; using System.Web.Http.Results; using System.Web.Http.Routing; using System.Web.Http.Validation; using Newtonsoft.Json; namespace System.Web.Http { public abstract class ApiController : IHttpController, IDisposable { private HttpActionContext _actionContext = new HttpActionContext(); private bool _initialized; /// Gets the configuration. /// The setter is intended for unit testing purposes only. public HttpConfiguration Configuration { get { return ControllerContext.Configuration; } set { ControllerContext.Configuration = value; } } /// Gets the controller context. /// The setter is intended for unit testing purposes only. public HttpControllerContext ControllerContext { get { // unit test only if (ActionContext.ControllerContext == null) { ActionContext.ControllerContext = new HttpControllerContext { RequestContext = new RequestBackedHttpRequestContext() }; } return ActionContext.ControllerContext; } set { if (value == null) { throw Error.PropertyNull(); } ActionContext.ControllerContext = value; } } /// Gets the action context. /// The setter is intended for unit testing purposes only. public HttpActionContext ActionContext { get { return _actionContext; } set { if (value == null) { throw Error.PropertyNull(); } _actionContext = value; } } /// /// Gets model state after the model binding process. This ModelState will be empty before model binding happens. /// /// The setter is intended for unit testing purposes only. public ModelStateDictionary ModelState { get { return ActionContext.ModelState; } } /// Gets or sets the HTTP request message. /// The setter is intended for unit testing purposes only. public HttpRequestMessage Request { get { return ControllerContext.Request; } set { if (value == null) { throw Error.PropertyNull(); } HttpRequestContext contextOnRequest = value.GetRequestContext(); HttpRequestContext contextOnController = RequestContext; if (contextOnRequest != null && contextOnRequest != contextOnController) { // Prevent unit testers from setting conflicting requests contexts. throw new InvalidOperationException(SRResources.RequestContextConflict); } ControllerContext.Request = value; value.SetRequestContext(contextOnController); RequestBackedHttpRequestContext requestBackedContext = contextOnController as RequestBackedHttpRequestContext; if (requestBackedContext != null) { requestBackedContext.Request = value; } } } /// Gets the request context. /// The setter is intended for unit testing purposes only. public HttpRequestContext RequestContext { get { return ControllerContext.RequestContext; } set { if (value == null) { throw Error.PropertyNull(); } HttpRequestContext oldContext = ControllerContext.RequestContext; HttpRequestMessage request = Request; if (request != null) { HttpRequestContext contextOnRequest = request.GetRequestContext(); if (contextOnRequest != null && contextOnRequest != oldContext && contextOnRequest != value) { // Prevent unit testers from setting conflicting requests contexts. throw new InvalidOperationException(SRResources.RequestContextConflict); } request.SetRequestContext(value); } ControllerContext.RequestContext = value; } } /// Gets a factory used to generate URLs to other APIs. /// The setter is intended for unit testing purposes only. public UrlHelper Url { get { return RequestContext.Url; } set { RequestContext.Url = value; } } /// Gets or sets the current principal associated with this request. /// The setter is intended for unit testing purposes only. public IPrincipal User { get { return RequestContext.Principal; } set { RequestContext.Principal = value; } } [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This method is a coordinator, so this coupling is expected.")] public virtual Task ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) { if (_initialized) { // 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; ServicesContainer controllerServices = controllerDescriptor.Configuration.Services; HttpActionDescriptor actionDescriptor = controllerServices.GetActionSelector().SelectAction(controllerContext); ActionContext.ActionDescriptor = actionDescriptor; if (Request != null) { Request.SetActionDescriptor(actionDescriptor); } FilterGrouping filterGrouping = actionDescriptor.GetFilterGrouping(); IActionFilter[] actionFilters = filterGrouping.ActionFilters; IAuthenticationFilter[] authenticationFilters = filterGrouping.AuthenticationFilters; IAuthorizationFilter[] authorizationFilters = filterGrouping.AuthorizationFilters; IExceptionFilter[] exceptionFilters = filterGrouping.ExceptionFilters; IHttpActionResult result = new ActionFilterResult(actionDescriptor.ActionBinding, ActionContext, controllerServices, actionFilters); if (authorizationFilters.Length > 0) { result = new AuthorizationFilterResult(ActionContext, authorizationFilters, result); } if (authenticationFilters.Length > 0) { result = new AuthenticationFilterResult(ActionContext, this, authenticationFilters, result); } if (exceptionFilters.Length > 0) { IExceptionLogger exceptionLogger = ExceptionServices.GetLogger(controllerServices); IExceptionHandler exceptionHandler = ExceptionServices.GetHandler(controllerServices); result = new ExceptionFilterResult(ActionContext, exceptionFilters, exceptionLogger, exceptionHandler, result); } return result.ExecuteAsync(cancellationToken); } /// /// Validates the given entity and adds the validation errors to the /// under the empty prefix, if any. /// /// The type of the entity to be validated. /// The entity being validated. public void Validate(TEntity entity) { Validate(entity, keyPrefix: String.Empty); } /// /// Validates the given entity and adds the validation errors to the , if any. /// /// The type of the entity to be validated. /// The entity being validated. /// /// The key prefix under which the model state errors would be added in the . /// public void Validate(TEntity entity, string keyPrefix) { if (Configuration == null) { throw Error.InvalidOperation(SRResources.TypePropertyMustNotBeNull, typeof(ApiController).Name, "Configuration"); } IBodyModelValidator validator = Configuration.Services.GetBodyModelValidator(); if (validator != null) { ModelMetadataProvider metadataProvider = Configuration.Services.GetModelMetadataProvider(); Contract.Assert(metadataProvider != null, "GetModelMetadataProvider throws on null."); validator.Validate(entity, typeof(TEntity), metadataProvider, ActionContext, keyPrefix); } } /// Creates a (400 Bad Request). /// A . protected internal virtual BadRequestResult BadRequest() { return new BadRequestResult(this); } /// /// Creates a (400 Bad Request) with the specified error message. /// /// The user-visible error message. /// A with the specified error message. protected internal virtual BadRequestErrorMessageResult BadRequest(string message) { return new BadRequestErrorMessageResult(message, this); } /// /// Creates an (400 Bad Request) with the specified model state. /// /// The model state to include in the error. /// An with the specified model state. protected internal virtual InvalidModelStateResult BadRequest(ModelStateDictionary modelState) { return new InvalidModelStateResult(modelState, this); } /// Creates a (409 Conflict). /// A . protected internal virtual ConflictResult Conflict() { return new ConflictResult(this); } /// Creates a with the specified values. /// The type of content in the entity body. /// The HTTP status code for the response message. /// The content value to negotiate and format in the entity body. /// A with the specified values. protected internal virtual NegotiatedContentResult Content(HttpStatusCode statusCode, T value) { return new NegotiatedContentResult(statusCode, value, this); } /// Creates a with the specified values. /// The type of content in the entity body. /// The HTTP status code for the response message. /// The content value to format in the entity body. /// The formatter to use to format the content. /// A with the specified values. protected internal FormattedContentResult Content(HttpStatusCode statusCode, T value, MediaTypeFormatter formatter) { return Content(statusCode, value, formatter, (MediaTypeHeaderValue)null); } /// Creates a with the specified values. /// The type of content in the entity body. /// The HTTP status code for the response message. /// The content value to format in the entity body. /// The formatter to use to format the content. /// The value for the Content-Type header. /// A with the specified values. protected internal FormattedContentResult Content(HttpStatusCode statusCode, T value, MediaTypeFormatter formatter, string mediaType) { return Content(statusCode, value, formatter, new MediaTypeHeaderValue(mediaType)); } /// Creates a with the specified values. /// The type of content in the entity body. /// The HTTP status code for the response message. /// The content value to format in the entity body. /// The formatter to use to format the content. /// /// The value for the Content-Type header, or to have the formatter pick a default /// value. /// /// A with the specified values. protected internal virtual FormattedContentResult Content(HttpStatusCode statusCode, T value, MediaTypeFormatter formatter, MediaTypeHeaderValue mediaType) { return new FormattedContentResult(statusCode, value, formatter, mediaType, this); } /// /// Creates a (201 Created) with the specified values. /// /// The type of content in the entity body. /// /// The location at which the content has been created. Must be a relative or absolute URL. /// /// The content value to negotiate and format in the entity body. /// A with the specified values. protected internal CreatedNegotiatedContentResult Created(string location, T content) { if (location == null) { throw new ArgumentNullException("location"); } return Created(new Uri(location, UriKind.RelativeOrAbsolute), content); } /// /// Creates a (201 Created) with the specified values. /// /// The type of content in the entity body. /// The location at which the content has been created. /// The content value to negotiate and format in the entity body. /// A with the specified values. protected internal virtual CreatedNegotiatedContentResult Created(Uri location, T content) { return new CreatedNegotiatedContentResult(location, content, this); } /// /// Creates a (201 Created) with the specified values. /// /// The type of content in the entity body. /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// The content value to negotiate and format in the entity body. /// A with the specified values. protected internal CreatedAtRouteNegotiatedContentResult CreatedAtRoute(string routeName, object routeValues, T content) { return CreatedAtRoute(routeName, new HttpRouteValueDictionary(routeValues), content); } /// /// Creates a (201 Created) with the specified values. /// /// The type of content in the entity body. /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// The content value to negotiate and format in the entity body. /// A with the specified values. protected internal virtual CreatedAtRouteNegotiatedContentResult CreatedAtRoute(string routeName, IDictionary routeValues, T content) { return new CreatedAtRouteNegotiatedContentResult(routeName, routeValues, content, this); } /// Creates an (500 Internal Server Error). /// A . protected internal virtual InternalServerErrorResult InternalServerError() { return new InternalServerErrorResult(this); } /// /// Creates an (500 Internal Server Error) with the specified exception. /// /// The exception to include in the error. /// An with the specified exception. protected internal virtual ExceptionResult InternalServerError(Exception exception) { return new ExceptionResult(exception, this); } /// Creates a (200 OK) with the specified value. /// The type of content in the entity body. /// The content value to serialize in the entity body. /// A with the specified value. protected internal JsonResult Json(T content) { return Json(content, new JsonSerializerSettings()); } /// Creates a (200 OK) with the specified values. /// The type of content in the entity body. /// The content value to serialize in the entity body. /// The serializer settings. /// A with the specified values. protected internal JsonResult Json(T content, JsonSerializerSettings serializerSettings) { return Json(content, serializerSettings, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); } /// Creates a (200 OK) with the specified values. /// The type of content in the entity body. /// The content value to serialize in the entity body. /// The serializer settings. /// The content encoding. /// A with the specified values. protected internal virtual JsonResult Json(T content, JsonSerializerSettings serializerSettings, Encoding encoding) { return new JsonResult(content, serializerSettings, encoding, this); } /// Creates a (404 Not Found). /// A . protected internal virtual NotFoundResult NotFound() { return new NotFoundResult(this); } /// Creates an (200 OK). /// An . protected internal virtual OkResult Ok() { return new OkResult(this); } /// /// Creates an (200 OK) with the specified values. /// /// The type of content in the entity body. /// The content value to negotiate and format in the entity body. /// An with the specified values. protected internal virtual OkNegotiatedContentResult Ok(T content) { return new OkNegotiatedContentResult(content, this); } /// Creates a (302 Found) with the specified value. /// The location to which to redirect. /// A with the specified value. protected internal virtual RedirectResult Redirect(string location) { if (location == null) { throw new ArgumentNullException("location"); } return Redirect(new Uri(location)); } /// Creates a (302 Found) with the specified value. /// The location to which to redirect. /// A with the specified value. protected internal virtual RedirectResult Redirect(Uri location) { return new RedirectResult(location, this); } /// Creates a (302 Found) with the specified values. /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// A with the specified values. protected internal RedirectToRouteResult RedirectToRoute(string routeName, object routeValues) { return RedirectToRoute(routeName, new HttpRouteValueDictionary(routeValues)); } /// Creates a (302 Found) with the specified values. /// The name of the route to use for generating the URL. /// The route data to use for generating the URL. /// A with the specified values. protected internal virtual RedirectToRouteResult RedirectToRoute(string routeName, IDictionary routeValues) { return new RedirectToRouteResult(routeName, routeValues, this); } /// Creates a with the specified response. /// The HTTP response message. /// A for the specified response. protected internal virtual ResponseMessageResult ResponseMessage(HttpResponseMessage response) { return new ResponseMessageResult(response); } /// Creates a with the specified status code. /// The HTTP status code for the response message /// A with the specified status code. protected internal virtual StatusCodeResult StatusCode(HttpStatusCode status) { return new StatusCodeResult(status, this); } /// /// Creates an (401 Unauthorized) with the specified values. /// /// The WWW-Authenticate challenges. /// An with the specified values. protected internal UnauthorizedResult Unauthorized(params AuthenticationHeaderValue[] challenges) { return Unauthorized((IEnumerable)challenges); } /// /// Creates an (401 Unauthorized) with the specified values. /// /// The WWW-Authenticate challenges. /// An with the specified values. protected internal virtual UnauthorizedResult Unauthorized(IEnumerable challenges) { return new UnauthorizedResult(challenges, this); } protected virtual void Initialize(HttpControllerContext controllerContext) { if (controllerContext == null) { throw Error.ArgumentNull("controllerContext"); } _initialized = true; ControllerContext = controllerContext; } #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } #endregion IDisposable } }