Azure DevOps Rest API - Batch Requests

Introduction

In my last post, I covered the basics of retrieving and updating work items. In this post, I am going to cover an additional work item API that you should use if you are creating or updating work items in bulk.

NuGet Prerequisites

You will need the following two NuGet packages to be able to access the Batch Request API.

Batch Request API Overview

To create or update a batch of work items, you can use the following method. This method takes a list of WitBatchRequests to represent each operation you want to complete as part of the batch. For each request, the Method will be PATCH, and only one header is needed to set the Content-Type to application/json-patch+json. The Body will be a JsonPatchDocument, just like when you are updating a single work item. The last part it the Uri, and I will go into that in the next section. Additional documentation about this endpoint, including samples of some common operations, can be found here.

public async Task<List<WitBatchResponse>> ExecuteBatchRequest(
    IEnumerable<WitBatchRequest> requests,
    object userState = null,
    CancellationToken cancellationToken = default (CancellationToken))

Batch Request URLs

For each request, you need to provide a URL that Azure DevOps will use when executing the request. For all requests, you need to include api-version=4.1 in the query string to ensure that it is using a version of the API that supports batch requests.

Create Work Item URLs

When building the URL for a create request, you need to start with the name of the project, followed by a /_apis/wit/workItems/ fragment. The final part is the type of the work item that you are creating, prefixed by a $. Here is an example of a URL to create a Task in a project named Sandbox.

/Sandbox/_apis/wit/workItems/$Task?api-version=4.1

Update Work Item URLs

Work items IDs in Azure DevOps are shared across projects, and so when you are building a URL for an update request, you do not need to include the project name like you do for create requests. Instead, you start with the same fragment as create requests (/_apis/wit/workItems/), and then include the ID of the work item that you want to update. Here is an example of updating a work item with the ID of 123.

/_apis/wit/workItems/123?api-version=4.1

Create Work Items

Next, I will demonstrate how you can use the batch request API to create multiple work items by adding a new command to the AzureDevOpsCLI tool. I am going to add a command that will take a parent work item ID, as well as a list of task names. It will use that information to create children tasks under that work item. The code for this command is included below. There are a couple of things to call out about this code. First, if you are sending a batch of requests with multiple create requests, you need to include an operation that sets the ID to a unique negative value, so that Azure DevOps can distinguish between the requests. Also, when building a relationship link between work items, you need to provide a fully qualified URL to the related work item.

[Command("create-tasks", Description = "Creates one or more tasks for a work item")]
public class CreateTasksCommand : BaseAzureDevOpsProjectCommand
{
    [CommandOption("parent",
        Description = "The id of the work item that is the parent of the new tasks",
        IsRequired = true)]
    public int ParentWorkItemID { get; set; }

    [CommandOption("task-names",
        Description = "The names of the tasks to create",
        IsRequired = true)]
    public IEnumerable<string>? TaskNames { get; set; }

    protected override async ValueTask InternalExecuteAsync(IConsole console, VssConnection connection)
    {
        if (TaskNames == null || !TaskNames.Any())
            return;
        var client = connection.GetClient<WorkItemTrackingHttpClient>();
        var requests = TaskNames.Select((taskName, index) =>
        {
            var patch = new JsonPatchDocument
            {
                new JsonPatchOperation
                {
                    Operation = Operation.Add,
                    Path = "/id",
                    Value = -index - 1,
                },
                new JsonPatchOperation
                {
                    Operation = Operation.Add,
                    Path = "/fields/System.Title",
                    Value = taskName,
                },
                new JsonPatchOperation
                {
                    Operation = Operation.Add,
                    Path = "/relations/-",
                    Value = new WorkItemRelation
                    {
                        Rel = "System.LinkTypes.Hierarchy-Reverse",
                        Url = $"{BaseUrl}/_apis/wit/workItems/{ParentWorkItemID}"
                    }
                }
            };
            return new WitBatchRequest
            {
                Uri = $"/{Project}/_apis/wit/workItems/$Task?api-version=4.1",
                Method = "PATCH",
                Headers = new Dictionary<string, string> {{"Content-Type", "application/json-patch+json"}},
                Body = JsonConvert.SerializeObject(patch)
            };
        });
        var responses = await client.ExecuteBatchRequest(requests).ConfigureAwait(false);
        foreach (var response in responses)
        {
            var workItem = JsonConvert.DeserializeObject<WorkItem>(response.Body);
            await console.Output.WriteLineAsync($"{workItem.Id}: {workItem.Fields["System.Title"]}").ConfigureAwait(false);
        }
    }
}

Running this command will create the tasks, and output the IDs and titles of the new task

Create Tasks Example

Update Work Items

Next, I will create a new command to demonstrate updating existing work items. This command will take a list of work item IDs, and then assign those work items to the current user.

[Command("assign-work-items", Description = "Assigns one or more work items to the current user")]
public class AssignWorkItemsCommand : BaseAzureDevOpsProjectCommand
{
    [CommandOption("ids",
        Description = "The ids of the work items to assign.",
        IsRequired = true)]
    public IEnumerable<int>? IDs { get; set; }

    protected override async ValueTask InternalExecuteAsync(IConsole console, VssConnection connection)
    {
        if (IDs == null || !IDs.Any())
            return;
        var client = connection.GetClient<WorkItemTrackingHttpClient>();
        var requests = IDs.Select(id =>
        {
            var patch = new JsonPatchDocument
            {
                new JsonPatchOperation
                {
                    Operation = Operation.Add,
                    Path = "/fields/System.AssignedTo",
                    Value = connection.AuthenticatedIdentity.DisplayName,
                }
            };
            return new WitBatchRequest
            {
                Uri = $"/_apis/wit/workItems/{id}?api-version=4.1",
                Method = "PATCH",
                Headers = new Dictionary<string, string> {{"Content-Type", "application/json-patch+json"}},
                Body = JsonConvert.SerializeObject(patch)
            };
        });
        await client.ExecuteBatchRequest(requests).ConfigureAwait(false);
    }
}

Running this command, I am able to take those new tasks I just created and assign them to my user.

Assign Work Items Example

Conclusion

This post has covered the Azure DevOps Rest API to update multiple work items in a single request. This is useful when you are making bulk update, as it will reduce the number of server round trips that you need to make. It can also be useful when you are making updates that send notifications, as executing in a batch can prevent multiple emails about the same item.

No Comments