public async static Task <AttachmentReference> CreateAttachmentChunkedAsync(WorkItemTrackingHttpClient client, VssConnection connection, MemoryStream uploadStream, int chunkSizeInBytes) { // it's possible for the attachment to be empty, if so we can't used the chunked upload and need // to fallback to the normal upload path. if (uploadStream.Length == 0) { return(await CreateAttachmentAsync(client, uploadStream)); } var requestSettings = new VssHttpRequestSettings { SendTimeout = TimeSpan.FromMinutes(5) }; var httpClient = new HttpClient(new VssHttpMessageHandler(connection.Credentials, requestSettings)); // first create the attachment reference. // can't use the WorkItemTrackingHttpClient since it expects either a file or a stream. var attachmentReference = await RetryHelper.RetryAsync(async() => { var request = new HttpRequestMessage(HttpMethod.Post, $"{connection.Uri}/_apis/wit/attachments?uploadType=chunked&api-version=3.2"); var response = await httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { return(await response.Content.ReadAsAsync <AttachmentReference>()); } else { var exceptionResponse = await response.Content.ReadAsAsync <ExceptionResponse>(); throw new Exception(exceptionResponse.Message); } }, 5); // now send up each chunk var totalNumberOfBytes = uploadStream.Length; // if number of chunks divides evenly, no need to add an extra chunk var numberOfChunks = ClientHelpers.GetBatchCount(totalNumberOfBytes, chunkSizeInBytes); for (var i = 0; i < numberOfChunks; i++) { var chunkBytes = new byte[chunkSizeInBytes]; // offset is always 0 since read moves position forward var chunkLength = uploadStream.Read(chunkBytes, 0, chunkSizeInBytes); var result = await RetryHelper.RetryAsync(async() => { // manually create the request since the WorkItemTrackingHttpClient does not support chunking var content = new ByteArrayContent(chunkBytes, 0, chunkLength); content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); content.Headers.ContentLength = chunkLength; content.Headers.ContentRange = new ContentRangeHeaderValue(i *chunkSizeInBytes, i *chunkSizeInBytes + chunkLength - 1, totalNumberOfBytes); var chunkRequest = new HttpRequestMessage(HttpMethod.Put, $"{connection.Uri}/_apis/wit/attachments/" + attachmentReference.Id + "?uploadType=chunked&api-version=3.2") { Content = content }; var chunkResponse = await httpClient.SendAsync(chunkRequest); if (!chunkResponse.IsSuccessStatusCode) { // there are two formats for the exception, so detect both. var responseContentAsString = await chunkResponse.Content.ReadAsStringAsync(); var criticalException = JsonConvert.DeserializeObject <CriticalExceptionResponse>(responseContentAsString); var exception = JsonConvert.DeserializeObject <ExceptionResponse>(responseContentAsString); throw new Exception(criticalException.Value?.Message ?? exception.Message); } return(chunkResponse.StatusCode); }, 5); } return(attachmentReference); }