// 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.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Properties; namespace System.Web.Http.Batch { /// /// Default implementation of that encodes the HTTP request/response messages as MIME multipart. /// /// /// By default, it buffers the HTTP request messages in memory during parsing. /// public class DefaultHttpBatchHandler : HttpBatchHandler { private const string MultiPartContentSubtype = "mixed"; private const string MultiPartMixed = "multipart/mixed"; private BatchExecutionOrder _executionOrder; /// /// Initializes a new instance of the class. /// /// The for handling the individual batch requests. public DefaultHttpBatchHandler(HttpServer httpServer) : base(httpServer) { ExecutionOrder = BatchExecutionOrder.Sequential; SupportedContentTypes = new List() { MultiPartMixed }; } /// /// Gets or sets the execution order for the batch requests. The default execution order is sequential. /// /// value public BatchExecutionOrder ExecutionOrder { get { return _executionOrder; } set { if (!Enum.IsDefined(typeof(BatchExecutionOrder), value)) { throw new InvalidEnumArgumentException("value", (int)value, typeof(BatchExecutionOrder)); } _executionOrder = value; } } /// /// Gets the supported content types for the batch request. /// public IList SupportedContentTypes { get; private set; } /// /// Creates the batch response message. /// /// The responses for the batch requests. /// The original request containing all the batch requests. /// The token to monitor for cancellation requests. /// The batch response message. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing the object.")] public virtual Task CreateResponseMessageAsync(IList responses, HttpRequestMessage request, CancellationToken cancellationToken) { if (responses == null) { throw Error.ArgumentNull("responses"); } if (request == null) { throw Error.ArgumentNull("request"); } MultipartContent batchContent = new MultipartContent(MultiPartContentSubtype); foreach (HttpResponseMessage batchResponse in responses) { batchContent.Add(new HttpMessageContent(batchResponse)); } HttpResponseMessage response = request.CreateResponse(); response.Content = batchContent; return Task.FromResult(response); } /// public override async Task ProcessBatchAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { throw Error.ArgumentNull("request"); } ValidateRequest(request); IList subRequests = await ParseBatchRequestsAsync(request, cancellationToken); try { IList responses = await ExecuteRequestMessagesAsync(subRequests, cancellationToken); return await CreateResponseMessageAsync(responses, request, cancellationToken); } finally { foreach (HttpRequestMessage subRequest in subRequests) { request.RegisterForDispose(subRequest.GetResourcesForDisposal()); request.RegisterForDispose(subRequest); } } } /// /// Executes the batch request messages. /// /// The collection of batch request messages. /// The token to monitor for cancellation requests. /// A collection of for the batch requests. [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "We need to return a collection of response messages asynchronously.")] public virtual async Task> ExecuteRequestMessagesAsync(IEnumerable requests, CancellationToken cancellationToken) { if (requests == null) { throw Error.ArgumentNull("requests"); } List responses = new List(); try { switch (ExecutionOrder) { case BatchExecutionOrder.Sequential: foreach (HttpRequestMessage request in requests) { responses.Add(await Invoker.SendAsync(request, cancellationToken)); } break; case BatchExecutionOrder.NonSequential: responses.AddRange(await Task.WhenAll(requests.Select(request => Invoker.SendAsync(request, cancellationToken)))); break; } } catch { foreach (HttpResponseMessage response in responses) { if (response != null) { response.Dispose(); } } throw; } return responses; } /// /// Converts the incoming batch request into a collection of request messages. /// /// The request containing the batch request messages. /// The token to monitor for cancellation requests. /// A collection of . [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "We need to return a collection of request messages asynchronously.")] public virtual async Task> ParseBatchRequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { throw Error.ArgumentNull("request"); } List requests = new List(); cancellationToken.ThrowIfCancellationRequested(); MultipartStreamProvider streamProvider = await request.Content.ReadAsMultipartAsync(); foreach (HttpContent httpContent in streamProvider.Contents) { cancellationToken.ThrowIfCancellationRequested(); HttpRequestMessage innerRequest = request.RequestUri == null ? await httpContent.ReadAsHttpRequestMessageAsync() : await httpContent.ReadAsHttpRequestMessageAsync(request.RequestUri.Scheme); innerRequest.CopyBatchRequestProperties(request); requests.Add(innerRequest); } return requests; } /// /// Validates the incoming request that contains the batch request messages. /// /// The request containing the batch request messages. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing the object.")] public virtual void ValidateRequest(HttpRequestMessage request) { if (request == null) { throw Error.ArgumentNull("request"); } if (request.Content == null) { throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.BadRequest, SRResources.BatchRequestMissingContent)); } MediaTypeHeaderValue contentType = request.Content.Headers.ContentType; if (contentType == null) { throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.BadRequest, SRResources.BatchContentTypeMissing)); } if (!SupportedContentTypes.Contains(contentType.MediaType, StringComparer.OrdinalIgnoreCase)) { throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.BadRequest, Error.Format(SRResources.BatchMediaTypeNotSupported, contentType.MediaType))); } } } }