Azure DevOps Rest API - Work Items

Introduction

This post will walk through how you can access work items through the Azure DevOps Rest API. In this series of posts, I am creating a command line tool that can be used to demonstrate each of the APIs that I cover. The code for this tool is available on GitHub.

Installing the NuGet Package

To be able to access work items using the Azure DevOps Rest API, you will need to install the Microsoft.TeamFoundationServer.Client NuGet package. If you are starting a new application, you will also need to install the Microsoft.VisualStudio.Services.Client NuGet package.

You can install the package in a a number of different ways. If you are using Visual Studio, then you can install it through the Manage NuGet Packages menu option. Alternatively, you can install it through the Package Manager with this command:

Install-Package Microsoft.TeamFoundationServer.Client

Or through the .NET CLI with this command:

dotnet add package Microsoft.TeamFoundationServer.Client

Work Item API Overview

Interacting with work items is handled through the Work Item Tracking area of the DevOps Rest API. Microsoft has documentation of everything that you can access about work items at https://docs.microsoft.com/en-us/rest/api/azure/devops/wit.

You will first need to authenticate through the Azure DevOps Rest API. See my prior post for details on how to do that. Once you are authenticated, you can get access to the WorkItemTrackingHttpClient using the following code.

    var workItemClient = connection.GetClient<WorkItemTrackingHttpClient>();

Next, I will look at 3 different APIs that are available through the WorkItemTrackingHttpClient.

Retrieving a single work item.

To retrieve information about a single work item, you can use the following method. The project and id parameters are required, and are self-explanatory. By default, all of the fields of the work item are returned, but you can use the fields parameter to narrow that down to reduce the result payload. The asOf parameter allows you to specify a point-of-time in cases where you want to see what a work item looked like at a prior time. Like the fields parameter, the expand parameter is another way of controlling how much of the work item is hydrated in the result payload. Additional documentation about this endpoint can be found here.

public Task<WorkItem> GetWorkItemAsync(
    string project,
    int id,
    IEnumerable<string> fields = null,
    DateTime? asOf = null,
    WorkItemExpand? expand = null,
    object userState = null,
    CancellationToken cancellationToken = default (CancellationToken));

Adding the work-item command

Before adding the work-item command to the AzureDevOpsCLI tool, I first did a little refactoring to introduce some base classes so that I didn’t have to duplicate the common parameters to commands.

Here is the code for the new command. It is simply a matter of getting the client, retrieving the work item, and then writing information to the console.

[Command("work-item", Description = "Retrieve information about a work item")]
public class WorkItemCommand : BaseAzureDevOpsProjectCommand
{
    [CommandOption("id",
        Description = "The id of the work item to retrieve.",
        IsRequired = true)]
    public int ID { get; set; }

    protected override async ValueTask InternalExecuteAsync(IConsole console, VssConnection connection)
    {
        var client = connection.GetClient<WorkItemTrackingHttpClient>();
        var workItem = await client.GetWorkItemAsync(Project, ID).ConfigureAwait(false);
        await WorkItemWriter.WriteWorkItem(console, workItem).ConfigureAwait(false);
    }
}

And here is the code for the helper class for writing out details for a work item that will be reused in other commands.

public static class WorkItemWriter
{
    private static string? FieldOutput(object workItemField)
    {
        return workItemField switch
        {
            IdentityRef r => r.DisplayName,
            _ => workItemField?.ToString()
        };
    }

    public static async ValueTask WriteWorkItem(IConsole console, WorkItem workItem)
    {
        await console.Output.WriteLineAsync($"ID: {workItem.Id}").ConfigureAwait(false);
        foreach (var field in workItem.Fields.Keys)
        {
            var fieldOutput = FieldOutput(workItem.Fields[field]);
            await console.Output.WriteLineAsync($"{field}: {fieldOutput}").ConfigureAwait(false);
        }
    }
}

Here is an example of the output from the new work-item command.

Work Item Example

Retrieving via a WIQL query

If you want to get a list of work items based on some filtering criteria, you can use the QueryByWiqlAsync method. WIQL is a SQL like query language for querying work items in an Azure DevOps project. You can find more information about the WIQL syntax in the Microsoft documentation. Also, if you want to be able to see the underlying WIQL for an existing query, you can install the Wiql Editor from the marketplace, and it will give you an option in the query editor to import or export queries as WIQL.

This method takes a Wiql object, which is just a simple data class with a Query property. Additional documentation about this endpoint can be found here.

public Task<WorkItemQueryResult> QueryByWiqlAsync(
    Wiql wiql,
    bool? timePrecision = null,
    int? top = null,
    object userState = null,
    CancellationToken cancellationToken = default (CancellationToken));

This endpoint returns a list of work item IDs, but no details about those work items. In order to get the details, you could use the API I used earlier to get a single work item, but a better API to use would be this one that allows you to pass a list of work item IDs to get all of the work items in a single call. It is essentially the same as the single work item API except that it takes the list of IDs. Additional documentation for this endpoint can be found here.

public Task<List<WorkItem>> GetWorkItemsAsync(
    string project,
    IEnumerable<int> ids,
    IEnumerable<string> fields = null,
    DateTime? asOf = null,
    WorkItemExpand? expand = null,
    WorkItemErrorPolicy? errorPolicy = null,
    object userState = null,
    CancellationToken cancellationToken = default (CancellationToken));

Adding a my-work-items command

With this API, I can add a new command to the AzureDevOpsCLI tool that leverages these APIs. I am going to add a command that will return all of the incomplete work items that are assigned to my user. The code for the command can be found below. One thing to be aware of is that the GetWorkItemsAsync method will fail with a BadRequest error if you pass it an empty list of IDs, so that is something you want to check before calling it.

[Command("my-work-items", Description = "Retrieve work items assigned to your user account")]
public class MyWorkItemsCommand : BaseAzureDevOpsProjectCommand
{
    protected override async ValueTask InternalExecuteAsync(IConsole console, VssConnection connection)
    {
        var client = connection.GetClient<WorkItemTrackingHttpClient>();
        var wiql = new Wiql
        {
            Query = $@"
SELECT [System.Id]
FROM WorkItems
WHERE [System.TeamProject] = '{Project}'
AND [System.AssignedTo] = @me
AND [System.State] NOT IN ('Closed', 'Completed', 'Done', 'Removed')
"
        };
        var result = await client.QueryByWiqlAsync(wiql).ConfigureAwait(false);
        if (!result.WorkItems.Any())
            return;

        var workItemIDs = result.WorkItems.Select(workItem => workItem.Id);
        var workItems = await client.GetWorkItemsAsync(Project, workItemIDs).ConfigureAwait(false);
        foreach (var workItem in workItems)
        {
            await WorkItemWriter.WriteWorkItem(console, workItem).ConfigureAwait(false);
            await console.Output.WriteLineAsync().ConfigureAwait(false);
        }
    }

Here is the output from running this command.

My Work Items Example

Update a single work item

Up to this point, all of the APIs have been retrieving information about work items, but you can also update work items through the rest API. The primary method for updating is through the UpdateWorkItemsAsync method. This method takes a JsonPatchDocument that describes the updates that you want to make. Determining how to build the patch document can be a little challenging depending on what you are trying to update, but the documentation has some good examples that you can use to as a base.

public Task<WorkItem> UpdateWorkItemAsync(
    JsonPatchDocument document,
    int id,
    bool? validateOnly = null,
    bool? bypassRules = null,
    bool? suppressNotifications = null,
    WorkItemExpand? expand = null,
    object userState = null,
    CancellationToken cancellationToken = default (CancellationToken));

Adding a update-remaining-work command

Now I will demonstrate this API by adding a new command to the AzureDevOpsCLI tool to update the RemainingWork for a task. Below is the code for the command. This command takes the ID of the work item, and a CompletedWork amount that will be used to reduce the RemainingWork. First, I retrieve the work item using the same API we used earlier. Then I read off the existing RemainingWork value, subtract the CompletedWork from that value, and then build a JsonPatchDocument to update the field.

[Command("update-remaining-work", Description = "Update the remaining work for a task")]
public class UpdateRemainingWorkCommand : BaseAzureDevOpsProjectCommand
{
    [CommandOption("id",
        Description = "The id of the work item to update.",
        IsRequired = true)]
    public int ID { get; set; }
    [CommandOption("completed-work",
        Description = "The amount of hours completed.",
        IsRequired = true)]
    public int CompletedWork { get; set; }

    protected override async ValueTask InternalExecuteAsync(IConsole console, VssConnection connection)
    {
        var client = connection.GetClient<WorkItemTrackingHttpClient>();
        var workItem = await client.GetWorkItemAsync(Project, ID).ConfigureAwait(false);
        var remainingWork = Convert.ToInt32(workItem.Fields["Microsoft.VSTS.Scheduling.RemainingWork"]);
        await client.UpdateWorkItemAsync(new JsonPatchDocument
        {
            new JsonPatchOperation
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.Scheduling.RemainingWork",
                Value = remainingWork - CompletedWork,
            }
        }, ID).ConfigureAwait(false);
    }
}

First, I will run the command to retrieve the work item details.

Work Item Prior

Then I will run the command to update the remaining work.

Update Remaining Work Example

And then I will run the retrieve command again, and you can see that the RemainingWork was reduced.

Work Item After

Conclusion

This post has covered some of the common APIs for accessing work items using the Azure DevOps Rest API. Using these APIs allows you both retrieve and update work items and allow you to power automation and tools that access Azure DevOps.

No Comments