private async Task <(int created, int updated)> SaveChanges_Batch(bool commit, CancellationToken cancellationToken) { // see https://github.com/redarrowlabs/vsts-restapi-samplecode/blob/master/VSTSRestApiSamples/WorkItemTracking/Batch.cs // and https://docs.microsoft.com/en-us/rest/api/vsts/wit/workitembatchupdate?view=vsts-rest-4.1 // BUG this code won't work if there is a relation between a new (id<0) work item and an existing one (id>0): it is an API limit const string ApiVersion = "api-version=4.1"; int created = _context.Tracker.NewWorkItems.Count(); int updated = _context.Tracker.ChangedWorkItems.Count(); string baseUriString = _context.Client.BaseAddress.AbsoluteUri; BatchRequest[] batchRequests = new BatchRequest[created + updated]; Dictionary <string, string> headers = new Dictionary <string, string> { { "Content-Type", "application/json-patch+json" } }; string credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($":{_context.PersonalAccessToken}")); int index = 0; foreach (var item in _context.Tracker.NewWorkItems) { _context.Logger.WriteInfo($"Found a request for a new {item.WorkItemType} workitem in {item.TeamProject}"); batchRequests[index++] = new BatchRequest { method = "PATCH", uri = $"/{item.TeamProject}/_apis/wit/workitems/${item.WorkItemType}?{ApiVersion}", headers = headers, body = item.Changes .Where(c => c.Operation != Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Test) .ToArray() }; } foreach (var item in _context.Tracker.ChangedWorkItems) { _context.Logger.WriteInfo($"Found a request to update workitem {item.Id.Value} in {item.TeamProject}"); batchRequests[index++] = new BatchRequest { method = "PATCH", uri = FormattableString.Invariant($"/_apis/wit/workitems/{item.Id.Value}?{ApiVersion}"), headers = headers, body = item.Changes .Where(c => c.Operation != Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Test) .ToArray() }; } var converters = new JsonConverter[] { new JsonPatchOperationConverter() }; string requestBody = JsonConvert.SerializeObject(batchRequests, Formatting.Indented, converters); _context.Logger.WriteVerbose(requestBody); if (commit) { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); var batchRequest = new StringContent(requestBody, Encoding.UTF8, "application/json"); var method = new HttpMethod("POST"); // send the request var request = new HttpRequestMessage(method, $"{baseUriString}/_apis/wit/$batch?{ApiVersion}") { Content = batchRequest }; var response = await client.SendAsync(request, cancellationToken); if (response.IsSuccessStatusCode) { WorkItemBatchPostResponse batchResponse = await response.Content.ReadAsAsync <WorkItemBatchPostResponse>(cancellationToken); string stringResponse = JsonConvert.SerializeObject(batchResponse, Formatting.Indented); _context.Logger.WriteVerbose(stringResponse); bool succeeded = true; foreach (var batchElement in batchResponse.values) { if (batchElement.code != 200) { _context.Logger.WriteError($"Save failed: {batchElement.body}"); succeeded = false; } } if (!succeeded) { throw new InvalidOperationException("Save failed."); } } else { string stringResponse = await response.Content.ReadAsStringAsync(); _context.Logger.WriteError($"Save failed: {stringResponse}"); throw new InvalidOperationException($"Save failed: {response.ReasonPhrase}."); } }//using } else { _context.Logger.WriteWarning($"Dry-run mode: no updates sent to Azure DevOps."); }//if return(created, updated); }
internal async Task <WorkItemBatchPostResponse> InvokeAsync(BatchRequest[] batchRequests, CancellationToken cancellationToken) { string baseUriString = _context.Client.BaseAddress.AbsoluteUri; string credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($":{_context.PersonalAccessToken}")); var converters = new JsonConverter[] { new JsonPatchOperationConverter() }; string requestBody = JsonConvert.SerializeObject(batchRequests, Formatting.Indented, converters); _context.Logger.WriteVerbose($"Workitem(s) batch request:"); _context.Logger.WriteVerbose(requestBody); if (_commit) { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); var batchRequest = new StringContent(requestBody, Encoding.UTF8, "application/json"); var method = new HttpMethod("POST"); // send the request var request = new HttpRequestMessage(method, $"{baseUriString}/_apis/wit/$batch?{ApiVersion}") { Content = batchRequest }; var response = await client.SendAsync(request, cancellationToken); if (response.IsSuccessStatusCode) { WorkItemBatchPostResponse batchResponse = await response.Content.ReadAsAsync <WorkItemBatchPostResponse>(cancellationToken); string stringResponse = JsonConvert.SerializeObject(batchResponse, Formatting.Indented); _context.Logger.WriteVerbose($"Workitem(s) batch response:"); _context.Logger.WriteVerbose(stringResponse); bool succeeded = true; foreach (var batchElement in batchResponse.values) { if (batchElement.code != 200) { _context.Logger.WriteError($"Save failed: {batchElement.body}"); succeeded = false; } } if (!succeeded) { throw new InvalidOperationException($"Save failed."); } return(batchResponse); } else { string stringResponse = await response.Content.ReadAsStringAsync(); _context.Logger.WriteError($"Save failed: {stringResponse}"); throw new InvalidOperationException($"Save failed: {response.ReasonPhrase}."); } }//using } _context.Logger.WriteWarning($"Dry-run mode: no updates sent to Azure DevOps."); return(null); }
private async Task <(int created, int updated)> SaveChanges_TwoPhases(bool commit) { // see https://github.com/redarrowlabs/vsts-restapi-samplecode/blob/master/VSTSRestApiSamples/WorkItemTracking/Batch.cs // and https://docs.microsoft.com/en-us/rest/api/vsts/wit/workitembatchupdate?view=vsts-rest-4.1 // The workitembatchupdate API has a huge limit: // it fails adding a relation between a new (id<0) work item and an existing one (id>0) const string ApiVersion = "api-version=4.1"; string baseUriString = _context.Client.BaseAddress.AbsoluteUri; Dictionary <string, string> headers = new Dictionary <string, string>() { { "Content-Type", "application/json-patch+json" } }; string credentials = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes($":{_context.PersonalAccessToken}")); var converters = new JsonConverter[] { new JsonPatchOperationConverter() }; int created = _context.Tracker.NewWorkItems.Count(); int updated = _context.Tracker.ChangedWorkItems.Count(); BatchRequest[] newWorkItemsBatchRequests = new BatchRequest[created]; int index = 0; foreach (var item in _context.Tracker.NewWorkItems) { _context.Logger.WriteInfo($"Found a request for a new {item.WorkItemType} workitem in {_context.ProjectName}"); newWorkItemsBatchRequests[index++] = new BatchRequest { method = "PATCH", uri = $"/{_context.ProjectName}/_apis/wit/workitems/${item.WorkItemType}?{ApiVersion}", headers = headers, body = item.Changes .Where(c => c.Operation != Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Test) // remove relations as we might incour in API failure .Where(c => c.Path != "/relations/-") .ToArray() }; } string requestBody = JsonConvert.SerializeObject(newWorkItemsBatchRequests, Formatting.Indented, converters); _context.Logger.WriteVerbose($"New workitem(s) batch request:"); _context.Logger.WriteVerbose(requestBody); if (commit) { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); var batchRequest = new StringContent(requestBody, Encoding.UTF8, "application/json"); var method = new HttpMethod("POST"); // send the request var request = new HttpRequestMessage(method, $"{baseUriString}/_apis/wit/$batch?{ApiVersion}") { Content = batchRequest }; var response = client.SendAsync(request).Result; if (response.IsSuccessStatusCode) { WorkItemBatchPostResponse batchResponse = response.Content.ReadAsAsync <WorkItemBatchPostResponse>().Result; string stringResponse = JsonConvert.SerializeObject(batchResponse, Formatting.Indented); _context.Logger.WriteVerbose($"New workitem(s) batch response:"); _context.Logger.WriteVerbose(stringResponse); bool succeeded = true; foreach (var batchElement in batchResponse.values) { if (batchElement.code != 200) { _context.Logger.WriteError($"Save failed: {batchElement.body}"); succeeded = false; } } if (!succeeded) { throw new ApplicationException($"Save failed."); } else { _context.Logger.WriteVerbose($"Updating work item ids..."); // Fix back var realIds = new Dictionary <int, int>(); index = 0; foreach (var item in _context.Tracker.NewWorkItems) { int oldId = item.Id.Value; // the response order matches the request order string createdWorkitemJson = batchResponse.values[index++].body; dynamic createdWorkitemResult = JsonConvert.DeserializeObject(createdWorkitemJson); int newId = createdWorkitemResult.id; item.ReplaceIdAndResetChanges(item.Id.Value, newId); realIds.Add(oldId, newId); } foreach (var item in _context.Tracker.ChangedWorkItems) { item.RemapIdReferences(realIds); } } } else { string stringResponse = await response.Content.ReadAsStringAsync(); _context.Logger.WriteError($"Save failed: {stringResponse}"); throw new ApplicationException($"Save failed: {response.ReasonPhrase}."); } }//using } else { _context.Logger.WriteWarning($"Dry-run mode: no updates sent to Azure DevOps."); }//if var batchRequests = new List <BatchRequest>(); var allWorkItems = _context.Tracker.NewWorkItems.Concat(_context.Tracker.ChangedWorkItems); foreach (var item in allWorkItems) { var changes = item.Changes .Where(c => c.Operation != Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Test); if (changes.Any()) { _context.Logger.WriteInfo($"Found a request to update workitem {item.Id.Value} in {_context.ProjectName}"); batchRequests.Add(new BatchRequest { method = "PATCH", uri = $"/_apis/wit/workitems/{item.Id.Value}?{ApiVersion}", headers = headers, body = changes.ToArray() }); } } requestBody = JsonConvert.SerializeObject(batchRequests.ToArray(), Formatting.Indented, converters); _context.Logger.WriteVerbose($"Update workitem(s) batch request:"); _context.Logger.WriteVerbose(requestBody); if (commit) { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); var batchRequest = new StringContent(requestBody, Encoding.UTF8, "application/json"); var method = new HttpMethod("POST"); // send the request var request = new HttpRequestMessage(method, $"{baseUriString}/_apis/wit/$batch?{ApiVersion}") { Content = batchRequest }; var response = client.SendAsync(request).Result; if (response.IsSuccessStatusCode) { WorkItemBatchPostResponse batchResponse = response.Content.ReadAsAsync <WorkItemBatchPostResponse>().Result; string stringResponse = JsonConvert.SerializeObject(batchResponse, Formatting.Indented); _context.Logger.WriteVerbose(stringResponse); bool succeeded = true; foreach (var batchElement in batchResponse.values) { if (batchElement.code != 200) { _context.Logger.WriteError($"Save failed: {batchElement.body}"); succeeded = false; } } if (!succeeded) { throw new ApplicationException($"Save failed."); } } else { string stringResponse = await response.Content.ReadAsStringAsync(); _context.Logger.WriteError($"Save failed: {stringResponse}"); throw new ApplicationException($"Save failed: {response.ReasonPhrase}."); } }//using } else { _context.Logger.WriteWarning($"Dry-run mode: no updates sent to Azure DevOps."); }//if return(created, updated); }