Visual Studio Team Services

VSTS & TFS Rest API: 06 – Retrieving and Querying for existing Work Items

Covering Work Items will take multiple posts as this area provides and has lots of functionality and therefore also API endpoints and we’ll start with an easy one: retrieving existing Work Items.

Work Items are retrieved on a Team Project Collection level (albeit being part of a Team Project), hence you’ll get the corresponding WorkItemTrackingHttpClient like this:

// instantiate a vss connection using the Project Collection URI as Base Uri
var visualStudioServicesConnection = new VssConnection(new Uri(TeamProjectCollectionUri), new VssCredentials());

// Get a Work Item Tracking client
var workItemTrackingHttpClient = visualStudioServicesConnection.GetClient<WorkItemTrackingHttpClient>();

Having an instance of a WorkItemTrackingHttpClient, we can go ahead and retrieve Work Items in a couple different ways:

Retrieve Work Item(s) by Id(s)

Retrieving a single Work Item by Id

If you happen to know or retrieved the .Id of a single existing Work Item elsewhere, you can retrieve the corresponding Work Item instance by using the .GetWorkItemAsync(..) method, i.e. like so:

static readonly int ExistingWorkItemId = 1; // This is a single Work Item .Id known to exist (on my system)

// 1. Retrieve a single Work Item by its .Id
// #############################

// This will include Work Item Relations (work item links, test result associations etc) via the 'expand' parameter
// However, if any no Relations exist at all .Relations will be 'null' (rather than an empty List<>)
var workItemIncludingRelations = workItemTrackingHttpClient.GetWorkItemAsync(ExistingWorkItemId, expand: WorkItemExpand.Relations).Result;

// same as not specifying an 'expand' parameter at all - .Relations is 'null'
var workItemExcludingRelations = workItemTrackingHttpClient.GetWorkItemAsync(ExistingWorkItemId).Result;

As mentioned in the code comments above, there .GetWorkItemAsync(..) method has an ‘expand‘ parameter which is an enum of type WorkItemExpand which defines what sub-elements of the Work Item instance to retrieve along with it – nothing at all (.None), relations (.Relations – these are Work Item (Hyper-)Links and associations), Work Item Fields (.Fields) or all of the above (.All).

One side-note here: using WorkItemExpand.Relations as ‘expand‘ parameter value will include the Work Item relations, however, if the Work Item does not have any relations at all, the retrieved workItemInstance.Relations property will be ‘null’, not (as expected) an empty IList instance.

Personally I’d be fine with this property being null if I did specified to NOT retrieve relations upfront (i.e. WorkItemExpand.None) but otherwise I’d expect .Relations to represent what it is – an empty list.

Anyway, moving on to..

Retrieving multiple Work Items by their Ids

Similarly to single Work Items, you can retrieve multiple Work Items in bulk by using their .Id values like this:

static readonly List<int> ExistingWorkItemIds = new List<int>() { 1, 2, 3 ,4 };  // This is a list of Work Item .Id values known to exist (on my system)

// 2. Retrieve multiple Work Items by their .Ids
// #############################

// You can also retrieve multiple work items in bulk via their .Id values in one request to the VSTS/TFS backend like this:
var multipleWorkItemsRetrievedByTheirIds = workItemTrackingHttpClient.GetWorkItemsAsync(ExistingWorkItemIds, expand: WorkItemExpand.All).Result;

The signature of the .GetWorkItemsAsync() method is exactly the same as the one for single Work Items above, so nothing new here.

Retrieving Work Items via Queries

VSTS / TFS Queries are a powerful functionality to retrieve Work Items based on specified criteria and you can either create a query on the fly and run in or use (Personal or Shared) Queries stored on your server. Let’s start with the former:

(Dynamic) Queries utilizing the Work Item Query Language (WIQL)

The Work Item Query Language (WIQL) is a query syntax that reads a lot like SQL but provides a limited set of functionality and lacks more powerful operations like Joins and Subqueries and basically just allows querying for a flat list of work items, a one hop query for work items and linked work items and a tree query (for parent > child linked work items over multiple levels) and a most VSTS / TFS test artifacts like Test Plans, -Suites, Test Points and -Runs, but also Test Results and Attachments (more about all those in a later post) via SELECT statements (for ‘*’ or specific fields), optionally taking a WHERE predicate and an ORDER BY sorting clause.. and a few macros or as they are called now: variables.

That’s it. See the EBNF for the WIQL syntax to see what’s supported.

So a simple WIQL query might look like this:

Select * from WorkItems Where [System.State] <> 'Draft' And [System.CreatedAt] > '5/5/16 12:30'

For a full list of supported operators and more examples see the corresponding MSDN documentation and the slightly more up-to-date VSTS / TFS documentation.

Now that you have your query, you can use it to query for Work Items using the VSTS / TFS REST Api like this:

// 3. Query for Work Items

// 3.1 .. using a Wiql (Work Item Query Language) Query
var wiqlQuery = new Wiql() { Query = "Select * from WorkItems" };
var workItemQueryResultForWiqlBasedQuery = workItemTrackingHttpClient.QueryByWiqlAsync(wiqlQuery).Result;

var workItemsForQueryResultForWiqlBasedQuery = workItemTrackingHttpClient
        workItemQueryResultForWiqlBasedQuery.WorkItems.Select(workItemReference => workItemReference.Id),
        expand: WorkItemExpand.All).Result;

The .QueryByWiqlAsync(..) method is the interesting one here which takes a Wiql instance as parameter, which again only contains the query text. The .QueryByWiqlAsync(..) method also takes an optional (nullable) DateTime? ‘asOf‘ parameter which are so called Historical Queries that return the Work Items in their state at the given date (and time) rather than the latest & current state.

Stored Queries (Personal or Shared)

Stored Queries are, well – as the name already states, queries stored on the VSTS or TFS system – either only accessible by you (personal ones under ‘My Queries’) or Shared ones available and accessible by all users (which have the corresponding permissions of course).

These stored queries, personal or shared, can be run by their .Id, i.e. like so:

var resultsForStoredQuery = workItemTrackingHttpClient.QueryByIdAsync(storedQueryItem.Id).Result;

Which is also quite easy.. but it is a bit tricky to retrieve the .Id of those queries if you don’t happen to have it laying around. You can retrieve the stored queries via the API, too – it only comes with a caveat: stored Queries are represented by QueryHierarchyItem instances – but those can either be folders, or.. well.. queries. Folders because VSTS / TFS allows hierarchical structuring of those queries inside folders and sub-folders.

QueryHierarchyItem instances do however have a .IsFolder property which allows you to differentiate the two from one another.

So in order to iterate over the Stored Query Folders and their Queries, you can use an approach like this (simplified of course):

// 4. Stored Queries

// Retrieve stored Queries (which you / your authenticated user can see and access), up to 2 (sub-)levels or -hierarchies deep.
// .. it appears that (currently?) you can specify a max value of '2' for the 'depth' parameter which means you might need to 
// retrieve queries deeper in the hierarchy using another approach:
// > check the corresponding QueryHierarchyItem for its .HasChildren having a Value (.HasValue==true) and that Value being 'true' BUT
// .. the .Children being 'null'. Go ahead and use that QueryHierarchyItem's .Path value for the .GetQueryAsync(projectId, queryHierarchyItemPath, ...) method
// .. to drill down further into the hierarchy
var allStoredQueriesAccessibleByAuthenticatedUser = workItemTrackingHttpClient.GetQueriesAsync(ExistingProjectId, expand: QueryExpand.All, depth: 2, includeDeleted: false).Result;

// then go ahead and use these queries (this should in real code be placed in a proper method / set of method(s) obviously
foreach (var storedQueryItem in allStoredQueriesAccessibleByAuthenticatedUser)
    if (storedQueryItem.IsFolder.HasValue && storedQueryItem.IsFolder.Value == true)
        // this storedQueryItem is a Folder, it may have children.. or not
        if (storedQueryItem.HasChildren.HasValue && storedQueryItem.HasChildren.Value == true)
            if (storedQueryItem.Children != null)
                foreach (var childQueryItem in storedQueryItem.Children)
                    // iterate over child items.. and so and and so on and so on
                // this folder HAS children, but the deeper hierarchy hasn't been retrieved, yet
                // > see note above how to do just that
            // this query folder is empty
    else if (storedQueryItem.IsFolder.HasValue && storedQueryItem.IsFolder.Value == false)
        // this storedQueryItem is a query (and not a folder)
        // you can use it to run the query.. or access / modify etc its query statement
        var resultsForStoredQuery = workItemTrackingHttpClient.QueryByIdAsync(storedQueryItem.Id).Result;
        // this 'should' not happen
        throw new InvalidOperationException($"Well this is odd - QueryHierarchyItem '{storedQueryItem.Id}' is neither a folder, nor a query");

As documented in that code snippet, the somewhat cumbersome part is that you can only retrieve query folders / hierarchies 2 levels deep at most at any given time, so you will have to perform multiple API calls when navigating further down the hierarchy. That 2-level limit my change some time, but for now just keep in mind and you’ll be fine.


That’s it for retrieving Work Items, for now. We’ll be diving deeper into reading, updating and deleting Work Item Links and Associations (references) at a later post as well as working with Work Item Fields and updating them.


Comments disabled.