You’re here, so most likely you know that Azure DevOps includes a great platform for planning and managing software projects. And you also know that Azure DevOps is usually accessed via the Azure DevOps Portal. But do you know how to create a more streamlined process? In this post, we will explain how to automate the planning of work in Azure DevOps via the Azure DevOps API.
Let us begin with a little background information: In a recent project, we needed to onboard a large number of tenants for a multi-tenant solution. Each tenant was required to supply a complex set of information about e.g. design and modules needed. Once the tenant had supplied this information, next step was to create a set of tasks (you know, work items) in Azure DevOps, so that e.g. a designer could begin to implement the design specifications and a developer could begin to install and configure the requested modules.
“There must be a smarter way”, we thought. So, to streamline the process we created a custom onboarding flow as a web page. The design of the flow should be as easy and accessible as possible as well as include relevant explanations and validations. Once a tenant had completed the onboarding flow, we used the Azure DevOps API to automatically create the relevant work items.
This approach allowed us to onboard a large number of tenants in a very effective and consistent way; The onboarding flow and integration to Azure DevOps made sure that we had all information needed and that this information was available directly in the relevant work items in Azure DevOps.
“Fine”, you might think. “But how did you do it then?”. Well, we’ll tell you in a minute, but first, we have to elaborate on a few prerequisites, so we’re all aligned for what’s coming.
For the sake of simplicity of this post there are three prerequisites that need to be elaborated on. So, first things first: For the guide below, we have used a Personal Access Token (PAT) for authentication against Azure DevOps. You can create a PAT in Azure DevOps Portal under Security — and keep in mind, that any work item created with this PAT is tied to the user with whom the PAT is associated (hence personal…).
The second prerequisite to elaborate on, is that we have used the Azure CLI with Azure DevOps extensions to access the Azure DevOps API via PowerShell, using the command:
Example:
az devops
We did this, because it is a great way to get a feel of the API and retrieve the information needed in order to create work items automatically.
The third and final prerequisite to elaborate on before we get our hands dirty, is that we have used the ID of the project in which we wished to create work items, although it is possible to create a work item using the project-name as well. If you are using Azure CLI, you can get a list of projects and IDs using:
Example:
az devops project list
Now, as a last thing before the actual guide, a quick remark on the work item. The work item is a central concept in Azure DevOps representing an object (like a piece of work) that can be categorized, ordered and tracked all the way from planning to execution. Some conceptually different examples from Azure DevOps are i.e. Product Backlog Items, Tasks, and Bugs that are simply different types of work items ordered hierarchically.
If you have completely different work item type? Despair not! Azure DevOps supports a number of different work item types which can be customized to suit your process framework and needs. In this guide, we will create a simple Product Backlog Item with no parent work item and then continue to create a nested Task and add an attachment.
To get a thorough understanding of what constitutes a work item, we recommend that you start out by looking at an existing work item of the same type as the one you wish to create, using:
Example:
az boards work-item show --id [WORKITEMID]
The first thing you need to do, before creating a new work item, is to connect to Azure DevOps. Here’s how to do it:
Azure DevOps exposes a REST API that can be accessed via the different clients included in the Microsoft.TeamFoundationServer.Client NuGet package. If the name of the package seems confusing, remember, that Azure DevOps has gone through a number of name changes, and is currently a cloud-based version of the Team Foundation Server supporting the same API.
To access the Azure DevOps API (name aside), you need to create a connection first. Start by importing the following namespaces:
Example:
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
Then, create credentials and a connection using the PAT and the URL of your Azure DevOps instance – most likely https://dev.azure.com/[YOURORGANIZATION].
Example:
var credential = new VssBasicCredential("", [PAT]);
var connection = new VssConnection([URL], credential);
Finally, create a client from the connection. Microsoft.TeamFoundationServer.Client includes a number of different clients, but as we will be creating work items, we need a WorkItemTrackingHttpClient:
Example:
var client = connection.GetClient<WorkItemTrackingHttpClient>();
Now you are ready to create a new work item. As you can see from inspecting an existing work item using Azure CLI, a work item in Azure DevOps contains an ID, a revision number, a URL, a list of fields, and a list of relations. The ID, revision number, and URL are all controlled by Azure DevOps. As we will start out with creating a non-nested work item, we don’t need to worry about relations, so basically, we just have to construct a list of fields. Here’s how:
We need to supply a list of fields to the API. The list takes form of a JSON patch document represented by the JsonPatchDocument instance. As we will be including a number of add patch operations in the patch document, we start out by creating this extension method:
Example:
public static void AddPatch(this JsonPatchDocument document, string path, object value)
{
document.Add(new JsonPatchOperation
{
From = null,
Operation = Operation.Add,
Path = path,
Value = value
});
}
Now, we can add the fields we need to our patch document. The fields available depend on which work item type you are creating. The paths of the fields correspond to the paths found in the JSON version of the existing work item that is found using Azure CLI. In this case create a patch document and add the following fields:
Example:
var document = new JsonPatchDocument();
document.AddPatch("/fields/System.Title", "Title");
document.AddPatch("/fields/System.Description", "Description");
document.AddPatch("/fields/System.AssignedTo", "test@example.com");
Azure DevOps organizes work items in a nested structure of areas (e.g. boards) and iterations (e.g. sprints) depending on how you set up your project. You can create work items in a specific area and iteration using:
Example:
document.AddPatch("/fields/System.AreaPath", [AREAPATH]);
document.AddPatch("/fields/System.IterationPath", [ITERATIONPATH]);
To get the correct structure of iterations and areas particular to your project, we suggest that you inspect existing work items using the Azure DevOps Portal or get the information via Azure CLI. You can get a list of available areas using:
Example:
az boards area project list --project [PROJECTNAME] --depth [DEPTH]
And a list of iterations using:
Example:
az boards iteration project list --project [PROJECTNAME] --depth [DEPTH]
For a simple Product Backlog Item the fields above suffice, and we now send our patch document to the API including the project ID and the name of the work item type we wish to create. Here’s an example:
Example:
var workItemTask = client.CreateWorkItemAsync(document, [PROJECTID],
"Product Backlog Item");
As we wish to continue working with the newly created work item, however, we wait for the task to complete, and then return the result in the form of a WorkItem instance:
Example:
return workItemTask.Result;
Congrats! You have now created a new work item and should be able to see it using the Azure DevOps portal in the area and iteration in which you placed it. You can also retrieve the ID from the WorkItem instance and output the work item using:
Example:
az boards work-item show --id [WORKITEMID]
This allows you to get a better feel of how the work item is represented in Azure DevOps.
Work items in Azure DevOps exist by themselves in a nested hierarchy independent of areas and iterations, where one work item can include a number of sub work items. This hierarchy is established by adding relations to the JSON patch document. Let’s say that we want to add a new task of the work item type “Task” under the work item we created above (which we hence returned as a WorkItem instance called parentItem). We do this by creating a new patch document, add the appropriate fields, and include a relation to the parent work item:
Example:
document.AddPatch("/relations/-",
new
{
rel = "System.LinkTypes.Hierarchy-Reverse",
url = parentItem.Url,
attributes = new { name = "Parent"}
}
);
Then we send the new work item to the API:
Example:
var workItemTask =
client.CreateWorkItemAsync(document, [PROJECTID], "Task");
Again, if we wish to investigate the different relation types and their properties, we can use Azure CLI to output existing work items with relations and update our code accordingly. We can also get a list of all available relation types using:
Example:
az boards work-item relation list-type
Other types of relations are file attachments, which are created using the AttachedFile relation type. To upload the file to Azure DevOps, we call our client using:
Example:
var attachmentTask = client.CreateAttachmentAsync([STREAM],
[PROJECTID], [FILENAME]);var attachmentReference = attachmentTask.Result;
We can create a relation in our patch document using the attachmentReference returned from the API:
Example:
document.AddPatch("/relations/-",
new
{
rel = "AttachedFile",
url = attachmentReference.Url,
attributes = new { }
}
);
We now send the patch document to the API using the client and create a new task with the uploaded file attached.
In this post we have used Azure CLI to get information about area, iteration and work items. In many real-world scenarios we will need to access at least some of this information dynamically to e.g. place a work item in the current sprint. The clients supplied in the Microsoft.TeamFoundationServer.Client package let us retrieve all these information at run-time — and much more — and we recommend that you spend some time exploring these capabilities if you wish to dive deeper into interacting with Azure DevOps programmatically.
Full example:
using System;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;namespace AzureDevOps
{
class Program
{
private static string pat = “”;
private static Uri url = new Uri(“”);
private static string area = “”;
private static string iteration = “”;
private static string project = “”; static void Main(string[] args)
{
var credential = new VssBasicCredential("", pat);
var connection = new VssConnection(url, credential);
var client =
connection.GetClient<WorkItemTrackingHttpClient>();
var document = new JsonPatchDocument();
document.AddPatch("/fields/System.Title", "Title");
document.AddPatch("/fields/System.Description",
"Description");
document.AddPatch("/fields/System.AssignedTo",
"test@example.com");
document.AddPatch("/fields/System.AreaPath", area);
document.AddPatch("/fields/System.IterationPath",
iteration); var workItemTask =
client.CreateWorkItemAsync(document, project,
"Product Backlog Item"); Console.WriteLine(workItemTask.Result.Id);
}
} public static class Extensions
{
public static void AddPatch(this JsonPatchDocument document,
string path, object value)
{
document.Add(new JsonPatchOperation
{
From = null,
Operation = Operation.Add,
Path = path,
Value = value
});
}
}
}