| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | using System; |
| | using System.Collections.Generic; |
| | using System.IO; |
| | using System.Linq; |
| | using System.Net; |
| | using System.Net.Http; |
| | using System.Net.Http.Headers; |
| | using System.Text; |
| | using System.Threading; |
| | using System.Threading.Tasks; |
| | using System.Reflection; |
| |
|
| | using Google.Apis.Http; |
| | using Google.Apis.Services; |
| | using Google.Apis.Testing; |
| |
|
| | namespace Google.Apis.Requests |
| | { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | public sealed class BatchRequest |
| | { |
| | private const string DefaultBatchUrl = "https://www.googleapis.com/batch"; |
| | private const int QueueLimit = 1000; |
| |
|
| | private readonly IList<InnerRequest> allRequests = new List<InnerRequest>(); |
| |
|
| | private readonly string batchUrl; |
| | private readonly IClientService service; |
| |
|
| | |
| | internal string BatchUrl => batchUrl; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | public delegate void OnResponse<in TResponse> |
| | (TResponse content, RequestError error, int index, HttpResponseMessage message) where TResponse : class; |
| |
|
| | #region Inner Request |
| |
|
| | |
| | private class InnerRequest |
| | { |
| | |
| | public IClientServiceRequest ClientRequest { get; set; } |
| |
|
| | |
| | public Type ResponseType { get; set; } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | public virtual void OnResponse(object content, RequestError error, int index, HttpResponseMessage message) |
| | { |
| | |
| | var eTagValue = message.Headers.ETag != null ? message.Headers.ETag.Tag : null; |
| | var eTagContainer = content as IDirectResponseSchema; |
| | if (eTagContainer != null && eTagContainer.ETag == null && eTagValue != null) |
| | { |
| | eTagContainer.ETag = eTagValue; |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | private class InnerRequest<TResponse> : InnerRequest |
| | where TResponse : class |
| | { |
| | |
| | public OnResponse<TResponse> OnResponseCallback { get; set; } |
| |
|
| | public override void OnResponse(object content, RequestError error, int index, |
| | HttpResponseMessage message) |
| | { |
| | base.OnResponse(content, error, index, message); |
| | if (OnResponseCallback == null) |
| | return; |
| |
|
| | OnResponseCallback(content as TResponse, error, index, message); |
| | } |
| | } |
| |
|
| | #endregion |
| |
|
| | |
| | |
| | |
| | |
| | public BatchRequest(IClientService service) |
| | : this(service, (service as BaseClientService)?.BatchUri ?? DefaultBatchUrl) { } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | public BatchRequest(IClientService service, string batchUrl) |
| | { |
| | this.batchUrl = batchUrl; |
| | this.service = service; |
| | } |
| |
|
| | |
| | public int Count => allRequests.Count; |
| |
|
| | |
| | |
| | |
| | |
| | public void Queue<TResponse>(IClientServiceRequest request, OnResponse<TResponse> callback) |
| | where TResponse : class |
| | { |
| | if (Count > QueueLimit) |
| | { |
| | throw new InvalidOperationException("A batch request cannot contain more than 1000 single requests"); |
| | } |
| |
|
| | allRequests.Add(new InnerRequest<TResponse> |
| | { |
| | ClientRequest = request, |
| | ResponseType = typeof(TResponse), |
| | OnResponseCallback = callback, |
| | }); |
| | } |
| |
|
| | |
| | public Task ExecuteAsync() |
| | { |
| | return ExecuteAsync(CancellationToken.None); |
| | } |
| |
|
| | |
| | |
| | public async Task ExecuteAsync(CancellationToken cancellationToken) |
| | { |
| | if (Count == 0) |
| | { |
| | return; |
| | } |
| |
|
| | ConfigurableHttpClient httpClient = service.HttpClient; |
| |
|
| | var requests = from r in allRequests |
| | select r.ClientRequest; |
| | HttpContent outerContent = await CreateOuterRequestContent(requests).ConfigureAwait(false); |
| |
|
| | string fullContent; |
| | string boundary; |
| | using (var result = await httpClient.PostAsync(new Uri(batchUrl), outerContent, cancellationToken).ConfigureAwait(false)) |
| | { |
| | |
| | await EnsureSuccessAsync(result).ConfigureAwait(false); |
| |
|
| | |
| | const string boundaryKey = "boundary="; |
| | var contentType = result.Content.Headers.GetValues("Content-Type").First(); |
| | boundary = contentType.Substring(contentType.IndexOf(boundaryKey, StringComparison.Ordinal) + boundaryKey.Length); |
| | fullContent = await result.Content.ReadAsStringAsync().ConfigureAwait(false); |
| | } |
| |
|
| | int requestIndex = 0; |
| | |
| | while (true) |
| | { |
| | cancellationToken.ThrowIfCancellationRequested(); |
| |
|
| | var startIndex = fullContent.IndexOf("--" + boundary, StringComparison.Ordinal); |
| | if (startIndex == -1) |
| | { |
| | break; |
| | } |
| | fullContent = fullContent.Substring(startIndex + boundary.Length + 2); |
| | var endIndex = fullContent.IndexOf("--" + boundary, StringComparison.Ordinal); |
| | if (endIndex == -1) |
| | { |
| | break; |
| | } |
| |
|
| | HttpResponseMessage responseMessage = ParseAsHttpResponse(fullContent.Substring(0, endIndex)); |
| |
|
| | if (responseMessage.IsSuccessStatusCode) |
| | { |
| | |
| | var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); |
| | object deserializedContent = null; |
| | RequestError error = null; |
| | try |
| | { |
| | deserializedContent = service.Serializer.Deserialize(responseContent, |
| | allRequests[requestIndex].ResponseType); |
| | } |
| | catch (Exception ex) |
| | { |
| | error = new RequestError |
| | { |
| | Message = $"The response was read but could not be deserialized using the {nameof(service.Serializer)}.{Environment.NewLine}" + |
| | $"The exception thrown on deserializaton was:{Environment.NewLine}" + |
| | $"{ex}", |
| | }; |
| | } |
| |
|
| | allRequests[requestIndex].OnResponse(deserializedContent, error, requestIndex, responseMessage); |
| | } |
| | else |
| | { |
| | RequestError error; |
| | try |
| | { |
| | |
| | error = await service.DeserializeError(responseMessage).ConfigureAwait(false); |
| | } |
| | catch (GoogleApiException ex) when (ex.Error is object) |
| | { |
| | error = ex.Error; |
| | } |
| |
|
| | allRequests[requestIndex].OnResponse(null, error, requestIndex, responseMessage); |
| | } |
| |
|
| | requestIndex++; |
| | fullContent = fullContent.Substring(endIndex); |
| | } |
| | } |
| |
|
| | private async Task EnsureSuccessAsync(HttpResponseMessage result) |
| | { |
| | if (!result.IsSuccessStatusCode) |
| | { |
| | Exception innerException; |
| | try |
| | { |
| | |
| | RequestError error = await service.DeserializeError(result).ConfigureAwait(false); |
| | |
| | |
| | |
| | |
| | throw new GoogleApiException(service.Name) |
| | { |
| | Error = error, |
| | HttpStatusCode = result.StatusCode |
| | }; |
| | } |
| | catch (Exception ex) |
| | { |
| | |
| | |
| | |
| | |
| | innerException = ex; |
| | } |
| |
|
| | try |
| | { |
| | |
| | |
| | |
| | |
| | result.EnsureSuccessStatusCode(); |
| | } |
| | |
| | |
| | catch (HttpRequestException original) |
| | { |
| | throw new HttpRequestException(original.Message, innerException); |
| | } |
| | } |
| | } |
| |
|
| | |
| | [VisibleForTestOnly] |
| | internal static HttpResponseMessage ParseAsHttpResponse(string content) |
| | { |
| | var response = new HttpResponseMessage(); |
| |
|
| | using (var reader = new StringReader(content)) |
| | { |
| | string line = reader.ReadLine(); |
| |
|
| | |
| | while (string.IsNullOrEmpty(line)) |
| | { |
| | line = reader.ReadLine(); |
| | } |
| |
|
| | |
| | while (!string.IsNullOrEmpty(line)) |
| | { |
| | line = reader.ReadLine(); |
| | } |
| |
|
| | |
| | line = reader.ReadLine(); |
| | while (string.IsNullOrEmpty(line)) |
| | { |
| | line = reader.ReadLine(); |
| | } |
| | int code = int.Parse(line.Split(' ')[1]); |
| | response.StatusCode = (HttpStatusCode)code; |
| |
|
| | |
| | IDictionary<string, string> headersDic = new Dictionary<string, string>(); |
| | while (!string.IsNullOrEmpty((line = reader.ReadLine()))) |
| | { |
| | var separatorIndex = line.IndexOf(':'); |
| | var key = line.Substring(0, separatorIndex).Trim(); |
| | var value = line.Substring(separatorIndex + 1).Trim(); |
| | |
| | |
| | if (headersDic.ContainsKey(key)) |
| | { |
| | headersDic[key] = headersDic[key] + ", " + value; |
| | } |
| | else |
| | { |
| | headersDic.Add(key, value); |
| | } |
| | } |
| |
|
| | |
| | string mediaType = null; |
| | if (headersDic.ContainsKey("Content-Type")) |
| | { |
| | mediaType = headersDic["Content-Type"].Split(';', ' ')[0]; |
| | headersDic.Remove("Content-Type"); |
| | } |
| |
|
| | string contentBody = reader.ReadToEnd(); |
| | |
| | |
| | |
| | |
| | if (contentBody != "" || !string.IsNullOrEmpty(mediaType) || response.Content is null) |
| | { |
| | |
| | |
| | |
| | |
| | |
| | if (string.IsNullOrEmpty(mediaType)) |
| | { |
| | mediaType = "text/plain"; |
| | } |
| | response.Content = new StringContent(contentBody, Encoding.UTF8, mediaType); |
| | } |
| |
|
| | |
| | foreach (var keyValue in headersDic) |
| | { |
| | HttpHeaders headers = response.Headers; |
| | |
| | if (typeof(HttpContentHeaders).GetProperty(keyValue.Key.Replace("-", "")) != null) |
| | { |
| | headers = response.Content.Headers; |
| | } |
| |
|
| | |
| | |
| | if (!headers.TryAddWithoutValidation(keyValue.Key, keyValue.Value)) |
| | { |
| | throw new FormatException($"Could not parse header {keyValue.Key} from batch reply"); |
| | } |
| | } |
| |
|
| | |
| | |
| | } |
| |
|
| | return response; |
| | } |
| |
|
| | |
| | |
| | |
| | [VisibleForTestOnly] |
| | internal async static Task<HttpContent> CreateOuterRequestContent(IEnumerable<IClientServiceRequest> requests) |
| | { |
| | var mixedContent = new MultipartContent("mixed"); |
| | foreach (var request in requests) |
| | { |
| | mixedContent.Add(await CreateIndividualRequest(request).ConfigureAwait(false)); |
| | } |
| |
|
| | |
| | return mixedContent; |
| | } |
| |
|
| | |
| | [VisibleForTestOnly] |
| | internal static async Task<HttpContent> CreateIndividualRequest(IClientServiceRequest request) |
| | { |
| | HttpRequestMessage requestMessage = request.CreateRequest(false); |
| | string requestContent = await CreateRequestContentString(requestMessage).ConfigureAwait(false); |
| |
|
| | var content = new StringContent(requestContent); |
| | content.Headers.ContentType = new MediaTypeHeaderValue("application/http"); |
| | return content; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | [VisibleForTestOnly] |
| | internal static async Task<string> CreateRequestContentString(HttpRequestMessage requestMessage) |
| | { |
| | var sb = new StringBuilder(); |
| | sb.AppendFormat("{0} {1}", requestMessage.Method, requestMessage.RequestUri.AbsoluteUri); |
| |
|
| | |
| | foreach (var otherHeader in requestMessage.Headers) |
| | { |
| | sb.Append(Environment.NewLine) |
| | .AppendFormat("{0}: {1}", otherHeader.Key, string.Join(", ", otherHeader.Value.ToArray())); |
| | } |
| |
|
| | |
| | if (requestMessage.Content != null) |
| | { |
| | foreach (var contentHeader in requestMessage.Content.Headers) |
| | { |
| | sb.Append(Environment.NewLine) |
| | .AppendFormat("{0}: {1}", contentHeader.Key, string.Join(", ", contentHeader.Value.ToArray())); |
| | } |
| | } |
| |
|
| | |
| | if (requestMessage.Content != null) |
| | { |
| | sb.Append(Environment.NewLine); |
| | var content = await requestMessage.Content.ReadAsStringAsync().ConfigureAwait(false); |
| | sb.Append("Content-Length: ").Append(content.Length); |
| | sb.Append(Environment.NewLine).Append(Environment.NewLine).Append(content); |
| | } |
| |
|
| | return sb.Append(Environment.NewLine).ToString(); |
| | } |
| | } |
| | } |