// 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.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Dependencies; using System.Web.Http.Hosting; using System.Web.Http.ModelBinding; using System.Web.Http.Properties; using System.Web.Http.Results; using System.Web.Http.Routing; namespace System.Net.Http { /// /// Provides extension methods for the class. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class HttpRequestMessageExtensions { /// /// Gets the for the given request. /// /// The HTTP request. /// The . public static HttpConfiguration GetConfiguration(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { return requestContext.Configuration; } return request.LegacyGetConfiguration(); } internal static HttpConfiguration LegacyGetConfiguration(this HttpRequestMessage request) { return request.GetProperty(HttpPropertyKeys.HttpConfigurationKey); } /// /// Sets the for the given request. /// /// The HTTP request. /// The to set. public static void SetConfiguration(this HttpRequestMessage request, HttpConfiguration configuration) { if (request == null) { throw Error.ArgumentNull("request"); } if (configuration == null) { throw Error.ArgumentNull("configuration"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { requestContext.Configuration = configuration; } request.Properties[HttpPropertyKeys.HttpConfigurationKey] = configuration; } /// /// Gets the dependency resolver scope associated with this . /// Services which are retrieved from this scope will be released when the request is /// cleaned up by the framework. /// /// The HTTP request. /// The for the given request. public static IDependencyScope GetDependencyScope(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } IDependencyScope result; if (!request.Properties.TryGetValue(HttpPropertyKeys.DependencyScope, out result)) { IDependencyResolver dependencyResolver = request.GetConfiguration().DependencyResolver; result = dependencyResolver.BeginScope(); if (result == null) { throw Error.InvalidOperation(SRResources.DependencyResolver_BeginScopeReturnsNull, dependencyResolver.GetType().Name); } request.Properties[HttpPropertyKeys.DependencyScope] = result; request.RegisterForDispose(result); } return result; } /// Gets the associated with this request. /// The HTTP request. /// The associated with this request. public static HttpRequestContext GetRequestContext(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } return request.GetProperty(HttpPropertyKeys.RequestContextKey); } /// Gets an associated with this request. /// The HTTP request. /// The to associate with this request. public static void SetRequestContext(this HttpRequestMessage request, HttpRequestContext context) { if (request == null) { throw Error.ArgumentNull("request"); } if (context == null) { throw Error.ArgumentNull("context"); } request.Properties[HttpPropertyKeys.RequestContextKey] = context; } /// /// Gets the for the given request or null if not available. /// /// The HTTP request. /// The or null. public static SynchronizationContext GetSynchronizationContext(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } return request.GetProperty(HttpPropertyKeys.SynchronizationContextKey); } internal static void SetSynchronizationContext(this HttpRequestMessage request, SynchronizationContext synchronizationContext) { if (request == null) { throw Error.ArgumentNull("request"); } request.Properties[HttpPropertyKeys.SynchronizationContextKey] = synchronizationContext; } /// /// Gets the current or null if not available. /// /// The HTTP request. /// The or null. public static X509Certificate2 GetClientCertificate(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { return requestContext.ClientCertificate; } return request.LegacyGetClientCertificate(); } internal static X509Certificate2 LegacyGetClientCertificate(this HttpRequestMessage request) { X509Certificate2 result = null; if (!request.Properties.TryGetValue(HttpPropertyKeys.ClientCertificateKey, out result)) { // now let us get out the delegate and try to invoke it Func retrieveCertificate; if (request.Properties.TryGetValue(HttpPropertyKeys.RetrieveClientCertificateDelegateKey, out retrieveCertificate)) { result = retrieveCertificate(request); if (result != null) { request.Properties.Add(HttpPropertyKeys.ClientCertificateKey, result); } } } return result; } /// /// Gets the for the given request or null if not available. /// /// The HTTP request. /// The or null. public static IHttpRouteData GetRouteData(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { return requestContext.RouteData; } return request.LegacyGetRouteData(); } internal static IHttpRouteData LegacyGetRouteData(this HttpRequestMessage request) { return request.GetProperty(HttpPropertyKeys.HttpRouteDataKey); } /// /// Sets the for the given request. /// /// The HTTP request. /// The HTTP route data. public static void SetRouteData(this HttpRequestMessage request, IHttpRouteData routeData) { if (request == null) { throw Error.ArgumentNull("request"); } if (routeData == null) { throw Error.ArgumentNull("routeData"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { requestContext.RouteData = routeData; } request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; } /// /// Gets the selected for the given request or null if not available. /// /// The HTTP request. /// The or null. public static HttpActionDescriptor GetActionDescriptor(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } return request.GetProperty(HttpPropertyKeys.HttpActionDescriptorKey); } internal static void SetActionDescriptor(this HttpRequestMessage request, HttpActionDescriptor actionDescriptor) { if (request == null) { throw Error.ArgumentNull("request"); } request.Properties[HttpPropertyKeys.HttpActionDescriptorKey] = actionDescriptor; } private static T GetProperty(this HttpRequestMessage request, string key) { T value; request.Properties.TryGetValue(key, out value); return value; } /// /// Helper method for creating an message with a "416 (Requested Range Not Satisfiable)" status code. /// This response can be used in combination with the to indicate that the requested range or /// ranges do not overlap with the current resource. The response contains a "Content-Range" header indicating the valid upper and lower /// bounds for requested ranges. /// /// The request. /// An instance, typically thrown by a /// instance. /// An 416 (Requested Range Not Satisfiable) error response with a Content-Range header indicating the valid range. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller will dispose")] public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, InvalidByteRangeException invalidByteRangeException) { if (invalidByteRangeException == null) { throw Error.ArgumentNull("invalidByteRangeException"); } HttpResponseMessage rangeNotSatisfiableResponse = request.CreateErrorResponse(HttpStatusCode.RequestedRangeNotSatisfiable, invalidByteRangeException); rangeNotSatisfiableResponse.Content.Headers.ContentRange = invalidByteRangeException.ContentRange; return rangeNotSatisfiableResponse; } /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping an with message . /// If no formatter is found, this method returns a response with status 406 NotAcceptable. /// /// /// This method requires that has been associated with an instance of /// . /// /// The request. /// The status code of the created response. /// The error message. /// An error response with error message and status code . public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message) { return request.CreateErrorResponse(statusCode, new HttpError(message)); } /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping an with message /// and message detail .If no formatter is found, this method returns a response with /// status 406 NotAcceptable. /// /// /// This method requires that has been associated with an instance of /// . /// /// The request. /// The status code of the created response. /// The error message. This message will always be seen by clients. /// The error message detail. This message will only be seen by clients if we should include error detail. /// An error response with error message and message detail /// and status code . internal static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message, string messageDetail) { return request.CreateErrorResponse(statusCode, includeErrorDetail => includeErrorDetail ? new HttpError(message, messageDetail) : new HttpError(message)); } /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping an with error message /// for exception . If no formatter is found, this method returns a response with status 406 NotAcceptable. /// /// /// This method requires that has been associated with an instance of /// . /// /// The request. /// The status code of the created response. /// The error message. /// The exception. /// An error response for with error message /// and status code . public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message, Exception exception) { if (request == null) { throw Error.ArgumentNull("request"); } return request.CreateErrorResponse(statusCode, includeErrorDetail => new HttpError(exception, includeErrorDetail) { Message = message }); } /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping an for exception . /// If no formatter is found, this method returns a response with status 406 NotAcceptable. /// /// /// This method requires that has been associated with an instance of /// . /// /// The request. /// The status code of the created response. /// The exception. /// An error response for with status code . public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, Exception exception) { if (request == null) { throw Error.ArgumentNull("request"); } return request.CreateErrorResponse(statusCode, includeErrorDetail => new HttpError(exception, includeErrorDetail)); } /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping an for model state . /// If no formatter is found, this method returns a response with status 406 NotAcceptable. /// /// /// This method requires that has been associated with an instance of /// . /// /// The request. /// The status code of the created response. /// The model state. /// An error response for with status code . public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, ModelStateDictionary modelState) { if (request == null) { throw Error.ArgumentNull("request"); } return request.CreateErrorResponse(statusCode, includeErrorDetail => new HttpError(modelState, includeErrorDetail)); } /// /// Helper method that performs content negotiation and creates a representing an error /// with an instance of wrapping as the content. If no formatter /// is found, this method returns a response with status 406 NotAcceptable. /// /// /// This method requires that has been associated with an instance of /// . /// /// The request. /// The status code of the created response. /// The error to wrap. /// An error response wrapping with status code . public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, HttpError error) { if (request == null) { throw Error.ArgumentNull("request"); } return request.CreateErrorResponse(statusCode, includeErrorDetail => error); } private static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, Func errorCreator) { HttpConfiguration configuration = request.GetConfiguration(); HttpError error = errorCreator(request.ShouldIncludeErrorDetail()); // CreateErrorResponse should never fail, even if there is no configuration associated with the request // In that case, use the default HttpConfiguration to con-neg the response media type if (configuration == null) { using (HttpConfiguration defaultConfig = new HttpConfiguration()) { return request.CreateResponse(statusCode, error, defaultConfig); } } else { return request.CreateResponse(statusCode, error, configuration); } } /// /// Helper method that performs content negotiation and creates a with an instance /// of as the content and as the status code /// if a formatter can be found. If no formatter is found, this method returns a response with status 406 NotAcceptable. /// This forwards the call to with /// status code and a null configuration. /// /// /// This method requires that has been associated with an instance of /// . /// /// The type of the value. /// The request. /// The value to wrap. Can be null. /// A response wrapping with status code. public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, T value) { return request.CreateResponse(HttpStatusCode.OK, value, configuration: null); } /// /// Helper method that performs content negotiation and creates a with an instance /// of as the content if a formatter can be found. If no formatter is found, this /// method returns a response with status 406 NotAcceptable. This forwards the call to /// with a null /// configuration. /// /// /// This method requires that has been associated with an instance of /// . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value) { return request.CreateResponse(statusCode, value, configuration: null); } /// /// Helper method that performs content negotiation and creates a with an instance /// of as the content if a formatter can be found. If no formatter is found, this /// method returns a response with status 406 NotAcceptable. /// /// /// This method will use the provided or it will get the /// instance associated with . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// The configuration to use. Can be null. /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, HttpConfiguration configuration) { if (request == null) { throw Error.ArgumentNull("request"); } configuration = configuration ?? request.GetConfiguration(); if (configuration == null) { throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoConfiguration); } IContentNegotiator contentNegotiator = configuration.Services.GetContentNegotiator(); if (contentNegotiator == null) { throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoContentNegotiator, typeof(IContentNegotiator).FullName); } IEnumerable formatters = configuration.Formatters; return NegotiatedContentResult.Execute(statusCode, value, contentNegotiator, request, formatters); } /// /// Helper method that creates a with an instance containing the provided /// . The given is used to find an instance of . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// The media type used to look up an instance of . /// Thrown if the does not have an associated /// instance or if the configuration does not have a formatter matching . /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, string mediaType) { return request.CreateResponse(statusCode, value, new MediaTypeHeaderValue(mediaType)); } /// /// Helper method that creates a with an instance containing the provided /// . The given is used to find an instance of . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// The media type used to look up an instance of . /// Thrown if the does not have an associated /// instance or if the configuration does not have a formatter matching . /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, MediaTypeHeaderValue mediaType) { if (request == null) { throw Error.ArgumentNull("request"); } if (mediaType == null) { throw Error.ArgumentNull("mediaType"); } HttpConfiguration configuration = request.GetConfiguration(); if (configuration == null) { throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoConfiguration); } MediaTypeFormatter formatter = configuration.Formatters.FindWriter(typeof(T), mediaType); if (formatter == null) { throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoMatchingFormatter, mediaType, typeof(T).Name); } return request.CreateResponse(statusCode, value, formatter, mediaType); } /// /// Helper method that creates a with an instance containing the provided /// and the given . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// The formatter to use. /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, MediaTypeFormatter formatter) { return request.CreateResponse(statusCode, value, formatter, (MediaTypeHeaderValue)null); } /// /// Helper method that creates a with an instance containing the provided /// and the given . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// The formatter to use. /// The media type override to set on the response's content. Can be null. /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, MediaTypeFormatter formatter, string mediaType) { MediaTypeHeaderValue mediaTypeHeader = mediaType != null ? new MediaTypeHeaderValue(mediaType) : null; return request.CreateResponse(statusCode, value, formatter, mediaTypeHeader); } /// /// Helper method that creates a with an instance containing the provided /// and the given . /// /// The type of the value. /// The request. /// The status code of the created response. /// The value to wrap. Can be null. /// The formatter to use. /// The media type override to set on the response's content. Can be null. /// A response wrapping with . public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode, T value, MediaTypeFormatter formatter, MediaTypeHeaderValue mediaType) { if (request == null) { throw Error.ArgumentNull("request"); } if (formatter == null) { throw Error.ArgumentNull("formatter"); } return FormattedContentResult.Execute(statusCode, value, formatter, mediaType, request); } /// /// Adds the given to a list of resources that will be disposed by a host once /// the is disposed. /// /// The request controlling the lifecycle of . /// The resource to dispose when is being disposed. Can be null. public static void RegisterForDispose(this HttpRequestMessage request, IDisposable resource) { if (request == null) { throw Error.ArgumentNull("request"); } if (resource == null) { return; } List trackedResources = GetRegisteredResourcesForDispose(request); trackedResources.Add(resource); } /// /// Adds the given to a list of resources that will be disposed by a host once /// the is disposed. /// /// The request controlling the lifecycle of . /// The resources to dispose when is being disposed. Can be null. public static void RegisterForDispose(this HttpRequestMessage request, IEnumerable resources) { if (request == null) { throw Error.ArgumentNull("request"); } if (resources == null) { throw Error.ArgumentNull("resources"); } List trackedResources = GetRegisteredResourcesForDispose(request); foreach (IDisposable resource in resources) { if (resource != null) { trackedResources.Add(resource); } } } /// /// Disposes of all tracked resources associated with the which were added via the /// method. /// /// The request. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to ignore all exceptions.")] public static void DisposeRequestResources(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } List resourcesToDispose; if (request.Properties.TryGetValue(HttpPropertyKeys.DisposableRequestResourcesKey, out resourcesToDispose)) { foreach (IDisposable resource in resourcesToDispose) { try { resource.Dispose(); } catch { // ignore exceptions } } resourcesToDispose.Clear(); } } /// /// Retrieves the which has been assigned as the /// correlation id associated with the given . /// The value will be created and set the first time this method is called. /// /// The /// The associated with that request. public static Guid GetCorrelationId(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } Guid correlationId; if (!request.Properties.TryGetValue(HttpPropertyKeys.RequestCorrelationKey, out correlationId)) { // Check if the Correlation Manager ID is set; otherwise fallback to creating a new GUID correlationId = Trace.CorrelationManager.ActivityId; if (correlationId == Guid.Empty) { correlationId = Guid.NewGuid(); } request.Properties.Add(HttpPropertyKeys.RequestCorrelationKey, correlationId); } return correlationId; } /// /// Retrieves the parsed query string as a collection of key-value pairs. /// /// The /// The query string as a collection of key-value pairs. [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "NameValuePairsValueProvider takes an IEnumerable>")] public static IEnumerable> GetQueryNameValuePairs(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } Uri uri = request.RequestUri; // Unit tests may not always provide a Uri in the request if (uri == null || String.IsNullOrEmpty(uri.Query)) { return Enumerable.Empty>(); } IEnumerable> queryStringData; string cachedQueryString; request.Properties.TryGetValue>>(HttpPropertyKeys.RequestQueryNameValuePairsKey, out queryStringData); request.Properties.TryGetValue(HttpPropertyKeys.CachedRequestQueryKey, out cachedQueryString); if (queryStringData == null || (cachedQueryString != null && !Object.ReferenceEquals(cachedQueryString, uri.Query ?? String.Empty))) { FormDataCollection formData = new FormDataCollection(uri); // The ToArray call here avoids reparsing the query string, and avoids storing an Enumerator state // machine in the request state. queryStringData = formData.GetJQueryNameValuePairs().ToArray(); request.Properties[HttpPropertyKeys.RequestQueryNameValuePairsKey] = queryStringData; request.Properties[HttpPropertyKeys.CachedRequestQueryKey] = uri.Query ?? String.Empty; } return queryStringData; } /// /// Retrieves the instance associated with this request. /// /// The . /// The instance associated with this request. public static UrlHelper GetUrlHelper(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { return requestContext.Url; } return new UrlHelper(request); } /// /// Gets a value indicating whether the request originates from a local address or not. /// /// The HTTP request. /// if the request originates from a local address; otherwise, . public static bool IsLocal(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { return requestContext.IsLocal; } return request.LegacyIsLocal(); } internal static bool LegacyIsLocal(this HttpRequestMessage request) { Lazy isLocal = request.GetProperty>(HttpPropertyKeys.IsLocalKey); return isLocal == null ? false : isLocal.Value; } /// /// Gets a value indicating whether the request originates from a batch. /// /// The HTTP request. /// if the request originates from a batch; otherwise, . public static bool IsBatchRequest(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } return request.GetProperty(HttpPropertyKeys.IsBatchRequest); } /// /// Gets a value indicating whether error details, such as exception messages and stack traces, should be included for this HTTP request. /// /// The HTTP request. /// if the error details are to be included; otherwise, . public static bool ShouldIncludeErrorDetail(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } HttpRequestContext requestContext = GetRequestContext(request); if (requestContext != null) { return requestContext.IncludeErrorDetail; } return request.LegacyShouldIncludeErrorDetail(); } internal static bool LegacyShouldIncludeErrorDetail(this HttpRequestMessage request) { HttpConfiguration configuration = request.GetConfiguration(); IncludeErrorDetailPolicy includeErrorDetailPolicy = IncludeErrorDetailPolicy.Default; if (configuration != null) { includeErrorDetailPolicy = configuration.IncludeErrorDetailPolicy; } switch (includeErrorDetailPolicy) { case IncludeErrorDetailPolicy.Default: Lazy includeErrorDetail = request.GetProperty>(HttpPropertyKeys.IncludeErrorDetailKey); if (includeErrorDetail != null) { // If we are on webhost and the user hasn't changed the IncludeErrorDetailPolicy // look up into the Request's property bag else default to LocalOnly. return includeErrorDetail.Value; } goto case IncludeErrorDetailPolicy.LocalOnly; case IncludeErrorDetailPolicy.LocalOnly: return request.IsLocal(); case IncludeErrorDetailPolicy.Always: return true; case IncludeErrorDetailPolicy.Never: default: return false; } } /// /// Gets the collection of resources registered for dispose once the is disposed. /// /// The request. /// A collection of resources registered for dispose. public static IEnumerable GetResourcesForDisposal(this HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } return GetRegisteredResourcesForDispose(request); } private static List GetRegisteredResourcesForDispose(HttpRequestMessage request) { List registeredResourcesForDispose; if (!request.Properties.TryGetValue(HttpPropertyKeys.DisposableRequestResourcesKey, out registeredResourcesForDispose)) { registeredResourcesForDispose = new List(); request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey] = registeredResourcesForDispose; } return registeredResourcesForDispose; } } }