/// <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;
 }
Example #2
0
        /// <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);
            }
        }
Example #3
0
        /// <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();
            }
        }
Example #4
0
        /// <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);
        }
Example #6
0
        /// <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();
            }
        }