/// <summary> /// Set the batch operation type or throw if not allowed. /// </summary> /// <param name="operationType"> /// The type of operation to perform. /// </param> private void SetBatchOperationType(BlobBatchOperationType operationType) { if (Submitted) { throw BatchErrors.BatchAlreadySubmitted(); } else if (_operationType != null && _operationType != operationType) { throw BatchErrors.OnlyHomogenousOperationsAllowed(_operationType.Value); } _operationType = operationType; }
/// <summary> /// Split the batch multipart response into individual sub-operation /// responses and update the delayed responses already returned when /// the sub-operation was added. /// </summary> /// <param name="messages"> /// The batch sub-operation messages that were submitted. /// </param> /// <param name="rawResponse"> /// The raw batch response. /// </param> /// <param name="responseContent"> /// The raw multipart response content. /// </param> /// <param name="responseContentType"> /// The raw multipart response content type (containing the boundary). /// </param> /// <param name="throwOnAnyFailure"> /// A value indicating whether or not to throw exceptions for /// sub-operation failures. /// </param> /// <param name="async"> /// Whether to invoke the operation asynchronously. /// </param> /// <param name="cancellationToken"> /// Optional <see cref="CancellationToken"/> to propagate notifications /// that the operation should be cancelled. /// </param> /// <returns>A Task representing the update operation.</returns> private async Task UpdateOperationResponses( IList <HttpMessage> messages, Response rawResponse, Stream responseContent, string responseContentType, bool throwOnAnyFailure, bool async, CancellationToken cancellationToken) { // Parse the response content into individual responses Response[] responses; try { responses = await Multipart.ParseAsync( responseContent, responseContentType, async, cancellationToken) .ConfigureAwait(false); // Ensure we have the right number of responses if (messages.Count != responses.Length) { // If we get one response and it's a 400, this is the // service failing the entire batch and sending it back in // a format not currently documented by the spec if (responses.Length == 1 && responses[0].Status == 400) { // We'll re-process this response as a batch result throw ClientDiagnostics.CreateRequestFailedException(responses[0]); } else { throw BatchErrors.UnexpectedResponseCount(messages.Count, responses.Length); } } } catch (InvalidOperationException ex) { // Wrap any parsing errors in a RequestFailedException throw BatchErrors.InvalidResponse(ClientDiagnostics, rawResponse, ex); } // Update the delayed responses List <Exception> failures = new List <Exception>(); for (int i = 0; i < responses.Length; i++) { try { if (messages[i].TryGetProperty(BatchConstants.DelayedResponsePropertyName, out object value) && value is DelayedResponse response) { #pragma warning disable AZC0110 // DO NOT use await keyword in possibly synchronous scope. await response.SetLiveResponse(responses[i], throwOnAnyFailure).ConfigureAwait(false); #pragma warning restore AZC0110 // DO NOT use await keyword in possibly synchronous scope. } } catch (Exception ex) { failures.Add(ex); } } // Throw any failures if (failures.Count > 0) { throw BatchErrors.ResponseFailures(failures); } }
/// <summary> /// Submit a <see cref="BlobBatch"/> of sub-operations. /// </summary> /// <param name="batch"> /// A <see cref="BlobBatch"/> of sub-operations. /// </param> /// <param name="throwOnAnyFailure"> /// A value indicating whether or not to throw exceptions for /// sub-operation failures. /// </param> /// <param name="async"> /// Whether to invoke the operation asynchronously. /// </param> /// <param name="cancellationToken"> /// Optional <see cref="CancellationToken"/> to propagate notifications /// that the operation should be cancelled. /// </param> /// <returns> /// A <see cref="Response"/> on successfully submitting. /// </returns> /// <remarks> /// A <see cref="RequestFailedException"/> will be thrown if /// a failure to submit the batch occurs. Individual sub-operation /// failures will only throw if <paramref name="throwOnAnyFailure"/> is /// true and be wrapped in an <see cref="AggregateException"/>. /// </remarks> private async Task <Response> SubmitBatchInternal( BlobBatch batch, bool throwOnAnyFailure, bool async, CancellationToken cancellationToken) { DiagnosticScope scope = ClientDiagnostics.CreateScope($"{nameof(BlobBatchClient)}.{nameof(SubmitBatch)}"); try { scope.Start(); batch = batch ?? throw new ArgumentNullException(nameof(batch)); if (batch.Submitted) { throw BatchErrors.CannotResubmitBatch(nameof(batch)); } else if (!batch.IsAssociatedClient(this)) { throw BatchErrors.BatchClientDoesNotMatch(nameof(batch)); } // Get the sub-operation messages to submit IList <HttpMessage> messages = batch.GetMessagesToSubmit(); if (messages.Count == 0) { throw BatchErrors.CannotSubmitEmptyBatch(nameof(batch)); } // TODO: Consider validating the upper limit of 256 messages // Merge the sub-operations into a single multipart/mixed Stream (Stream content, string contentType) = await MergeOperationRequests( messages, async, cancellationToken) .ConfigureAwait(false); if (IsContainerScoped) { ResponseWithHeaders <Stream, ContainerSubmitBatchHeaders> response; if (async) { response = await _containerRestClient.SubmitBatchAsync( containerName : ContainerName, contentLength : content.Length, multipartContentType : contentType, body : content, cancellationToken : cancellationToken) .ConfigureAwait(false); } else { response = _containerRestClient.SubmitBatch( containerName: ContainerName, contentLength: content.Length, multipartContentType: contentType, body: content, cancellationToken: cancellationToken); } await UpdateOperationResponses( messages, response.GetRawResponse(), response.Value, response.Headers.ContentType, throwOnAnyFailure, async, cancellationToken) .ConfigureAwait(false); return(response.GetRawResponse()); } else { ResponseWithHeaders <Stream, ServiceSubmitBatchHeaders> response; if (async) { response = await _serviceRestClient.SubmitBatchAsync( contentLength : content.Length, multipartContentType : contentType, body : content, cancellationToken : cancellationToken) .ConfigureAwait(false); } else { response = _serviceRestClient.SubmitBatch( contentLength: content.Length, multipartContentType: contentType, body: content, cancellationToken: cancellationToken); } await UpdateOperationResponses( messages, response.GetRawResponse(), response.Value, response.Headers.ContentType, throwOnAnyFailure, async, cancellationToken) .ConfigureAwait(false); return(response.GetRawResponse()); } } catch (Exception ex) { scope.Failed(ex); throw; } finally { scope.Dispose(); } }
/// <summary> /// Submit a <see cref="BlobBatch"/> of sub-operations. /// </summary> /// <param name="batch"> /// A <see cref="BlobBatch"/> of sub-operations. /// </param> /// <param name="throwOnAnyFailure"> /// A value indicating whether or not to throw exceptions for /// sub-operation failures. /// </param> /// <param name="async"> /// Whether to invoke the operation asynchronously. /// </param> /// <param name="cancellationToken"> /// Optional <see cref="CancellationToken"/> to propagate notifications /// that the operation should be cancelled. /// </param> /// <returns> /// A <see cref="Response"/> on successfully submitting. /// </returns> /// <remarks> /// A <see cref="RequestFailedException"/> will be thrown if /// a failure to submit the batch occurs. Individual sub-operation /// failures will only throw if <paramref name="throwOnAnyFailure"/> is /// true and be wrapped in an <see cref="AggregateException"/>. /// </remarks> private async Task <Response> SubmitBatchInternal( BlobBatch batch, bool throwOnAnyFailure, bool async, CancellationToken cancellationToken) { batch = batch ?? throw new ArgumentNullException(nameof(batch)); if (batch.Submitted) { throw BatchErrors.CannotResubmitBatch(nameof(batch)); } else if (!batch.IsAssociatedClient(this)) { throw BatchErrors.BatchClientDoesNotMatch(nameof(batch)); } // Get the sub-operation messages to submit IList <HttpMessage> messages = batch.GetMessagesToSubmit(); if (messages.Count == 0) { throw BatchErrors.CannotSubmitEmptyBatch(nameof(batch)); } // TODO: Consider validating the upper limit of 256 messages // Merge the sub-operations into a single multipart/mixed Stream (Stream content, string contentType) = await MergeOperationRequests( messages, async, cancellationToken) .ConfigureAwait(false); // Send the batch request Response <BlobBatchResult> batchResult = await BatchRestClient.Service.SubmitBatchAsync( ClientDiagnostics, Pipeline, Uri, body : content, contentLength : content.Length, multipartContentType : contentType, async : async, operationName : BatchConstants.BatchOperationName, cancellationToken : cancellationToken) .ConfigureAwait(false); // Split the responses apart and update the sub-operation responses Response raw = batchResult.GetRawResponse(); await UpdateOperationResponses( messages, raw, batchResult.Value.Content, batchResult.Value.ContentType, throwOnAnyFailure, async, cancellationToken) .ConfigureAwait(false); // Return the batch result return(raw); }
/// <summary> /// Parse a multipart/mixed response body into several responses. /// </summary> /// <param name="batchContent">The response content.</param> /// <param name="batchContentType">The response content type.</param> /// <param name="async"> /// Whether to invoke the operation asynchronously. /// </param> /// <param name="cancellationToken"> /// Optional <see cref="CancellationToken"/> to propagate notifications /// that the operation should be cancelled. /// </param> /// <returns>The parsed <see cref="Response"/>s.</returns> public static async Task <Response[]> ParseAsync( Stream batchContent, string batchContentType, bool async, CancellationToken cancellationToken) { // Get the batch boundary if (batchContentType == null || !batchContentType.StartsWith(BatchConstants.MultipartContentTypePrefix, StringComparison.Ordinal)) { throw BatchErrors.InvalidBatchContentType(batchContentType); } string batchBoundary = batchContentType.Substring(BatchConstants.MultipartContentTypePrefix.Length); // Collect the responses in a dictionary (in case the Content-ID // values come back out of order) Dictionary <int, Response> responses = new Dictionary <int, Response>(); // Read through the batch body one section at a time until the // reader returns null MultipartReader reader = new MultipartReader(batchBoundary, batchContent); for (MultipartSection section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false); section != null; section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false)) { // Get the Content-ID header if (!section.Headers.TryGetValue(BatchConstants.ContentIdName, out StringValues contentIdValues) || contentIdValues.Count != 1 || !int.TryParse(contentIdValues[0], out int contentId)) { // If the header wasn't found, this is a failed request // with the details being sent as the first sub-operation // so we default the Content-ID to 0 contentId = 0; } // Build a response MemoryResponse response = new MemoryResponse(); responses[contentId] = response; // We're going to read the section's response body line by line using var body = new BufferedReadStream(section.Body, BatchConstants.ResponseLineSize); // The first line is the status like "HTTP/1.1 202 Accepted" string line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); string[] status = line.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); if (status.Length != 3) { throw BatchErrors.InvalidHttpStatusLine(line); } response.SetStatus(int.Parse(status[1], CultureInfo.InvariantCulture)); response.SetReasonPhrase(status[2]); // Continue reading headers until we reach a blank line line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); while (!string.IsNullOrEmpty(line)) { // Split the header into the name and value int splitIndex = line.IndexOf(':'); if (splitIndex <= 0) { throw BatchErrors.InvalidHttpHeaderLine(line); } var name = line.Substring(0, splitIndex); var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); response.AddHeader(name, value); line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); } // Copy the rest of the body as the response content var responseContent = new MemoryStream(); if (async) { await body.CopyToAsync(responseContent).ConfigureAwait(false); } else { body.CopyTo(responseContent); } responseContent.Seek(0, SeekOrigin.Begin); response.ContentStream = responseContent; } // Collect the responses and order by Content-ID Response[] ordered = new Response[responses.Count]; for (int i = 0; i < ordered.Length; i++) { ordered[i] = responses[i]; } return(ordered); }
/// <summary> /// Submit a <see cref="BlobBatch"/> of sub-operations. /// </summary> /// <param name="batch"> /// A <see cref="BlobBatch"/> of sub-operations. /// </param> /// <param name="throwOnAnyFailure"> /// A value indicating whether or not to throw exceptions for /// sub-operation failures. /// </param> /// <param name="async"> /// Whether to invoke the operation asynchronously. /// </param> /// <param name="cancellationToken"> /// Optional <see cref="CancellationToken"/> to propagate notifications /// that the operation should be cancelled. /// </param> /// <returns> /// A <see cref="Response"/> on successfully submitting. /// </returns> /// <remarks> /// A <see cref="RequestFailedException"/> will be thrown if /// a failure to submit the batch occurs. Individual sub-operation /// failures will only throw if <paramref name="throwOnAnyFailure"/> is /// true and be wrapped in an <see cref="AggregateException"/>. /// </remarks> private async Task <Response> SubmitBatchInternal( BlobBatch batch, bool throwOnAnyFailure, bool async, CancellationToken cancellationToken) { DiagnosticScope scope = ClientDiagnostics.CreateScope($"{nameof(BlobBatchClient)}.{nameof(SubmitBatch)}"); try { scope.Start(); batch = batch ?? throw new ArgumentNullException(nameof(batch)); if (batch.Submitted) { throw BatchErrors.CannotResubmitBatch(nameof(batch)); } else if (!batch.IsAssociatedClient(this)) { throw BatchErrors.BatchClientDoesNotMatch(nameof(batch)); } // Get the sub-operation messages to submit IList <HttpMessage> messages = batch.GetMessagesToSubmit(); if (messages.Count == 0) { throw BatchErrors.CannotSubmitEmptyBatch(nameof(batch)); } // TODO: Consider validating the upper limit of 256 messages // Merge the sub-operations into a single multipart/mixed Stream (Stream content, string contentType) = await MergeOperationRequests( messages, async, cancellationToken) .ConfigureAwait(false); // Send the batch request Response <BlobBatchResult> batchResult; if (_isContainerScoped) { batchResult = await BatchRestClient.Container.SubmitBatchAsync( clientDiagnostics : ClientDiagnostics, pipeline : Pipeline, resourceUri : Uri, body : content, contentLength : content.Length, multipartContentType : contentType, version : Version.ToVersionString(), async : async, operationName : $"{nameof(BlobBatchClient)}.{nameof(SubmitBatch)}", cancellationToken : cancellationToken) .ConfigureAwait(false); } else { batchResult = await BatchRestClient.Service.SubmitBatchAsync( clientDiagnostics : ClientDiagnostics, pipeline : Pipeline, resourceUri : Uri, body : content, contentLength : content.Length, multipartContentType : contentType, version : Version.ToVersionString(), async : async, operationName : $"{nameof(BlobBatchClient)}.{nameof(SubmitBatch)}", cancellationToken : cancellationToken) .ConfigureAwait(false); } // Split the responses apart and update the sub-operation responses Response raw = batchResult.GetRawResponse(); await UpdateOperationResponses( messages, raw, batchResult.Value.Content, batchResult.Value.ContentType, throwOnAnyFailure, async, cancellationToken) .ConfigureAwait(false); // Return the batch result return(raw); } catch (Exception ex) { scope.Failed(ex); throw; } finally { scope.Dispose(); } }