Team Foundation Server

Local Process Template Location on TFS Server

Just as a little reminder as I was looking for that information myself again recently, if you need to access the latest version of the TFS on-premises Process Templates and you cannot access them via Visual Studio’s Process Template Manager or the correspondong command line tool(s), they are also available on the Team Foundation Server under

%programfiles%\Microsoft Team Foundation Server 14.0\Tools\Deploy\ProcessTemplateManagerFiles\1033

You probably would / should not make any changes there directly, but that’s where they are stored. The ‘14.0‘ in the path above is the version number of TFS, so this will change in the future correspondingly (TFS2015 is version 14.0, the next version, apparently TFS2016, will be ‘15.0’ and so on).

Team Foundation Server

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
    .GetWorkItemsAsync(
        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
                }
            }
            else
            {
                // this folder HAS children, but the deeper hierarchy hasn't been retrieved, yet
                // > see note above how to do just that
            }
        }
        else
        {
            // 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;
    }
    else
    {
        // 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.

Conclusion

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.

Microsoft

Navigating to Team Explorer Pages and passing along Context & Parameters

When creating your own VSTS / TFS Visual Studio Extension you may want to navigate to Team Explorer pages, either to your own or to built-in ones like Code Reviews, TFVC Changeset or Git Commit Details.

The corresponding method to do so is the ITeamExplorer.NavigateToPage() one which takes the target page’s .Id as parameter and an context one. The former are usually quite easy to get ahold of – when creating and providing your own Team Explorer navigation and page items you must provide and therefore know their corresponding Ids but the VS built-in pages’ Ids are also available in a central location named TeamExplorerPageIds.

Providing the right context (type) however may be a bit more tricky. As long as you know the target page does not require any context (and can handle null properly), you can go ahead and navigate to it i.e. like this:

var teamExplorer = serviceProvider.GetService(typeof(ITeamExplorer)) as ITeamExplorer;

teamExplorer.NavigateToPage(new Guid(TeamExplorerPageIds.MyWork), null);

However, if the target page does require a context to be passed in, having an object as parameter type obfuscates what the target page is expecting and unfortunately Visual Studio’s own Team Explorer pages’ parameters aren’t documented.. or the documentation is well-hidden. I’ll list the ones I know below, but for your own Team Explorer pages you can obviously pass along whatever context parameter you deem necessary when navigating to it and you’ll get it handed over in the ITeamExplorerPage.Initialize() method you have to implement for your page, more particularly its PageInitializeEventArgs.Context parameter.

Users may obviously navigate away from your page (or change connections, projects etc) and whenever that happens, your ITeamExplorer.SaveContext() method is called which allows you to store the current context for your page. It’s up to you how broad or narrow you define the term context here – it may for example only be a work item .Id but you may also use context more broadly and save visual state of your page along with it.

As for the aforementioned core Visual Studio Team Explorer pages, I’ve come across the following contexts you can or have to provide when navigating to the corresponding pages & unless mentioned otherwise, the ITeamExplorer.NavigateToPage(pageId, context) parameter is always a Dictionary<string, object> – the keys and value(types) being:

  • Builds
    • Key: “QueuedBuildId
    • Value: <Build Id> (int)
  • Request Code Review
    • Key: “Workspace
    • Value: <Workspace instance> (Workspace)
      or
    • Key: “ChangesetId
    • Value: <Changeset Id> (int, see Changeset.ChangesetId)
      or
    • Key: “Shelveset
    • Value: <Shelveset Instance> (Shelveset)
      or
    • Key: “ShelvesetName
    • Value: <Shelveset Name> (string, see Shelveset.Name)Please Note: for both, “Shelveset” and “ShelvesetName” there’s a second Dictionary key-value pair that you should provide:
    • Key: “ExcludedCount
    • Value: “<Count of excluded Changes from Shelveset>” (int, why this parameter is required is unbeknownst to me however)
  • View Code Review
    • Key: “WorkItem
    • Value: <Work Item Instance> (WorkItem)
      or
    • Key: “WorkItemId
    • Value: <Work Item Id> (int OR string)
  • Changeset Details
    • Cannot be called (unless using Reflection, which I’d not recommend), the context is an instance of type ‘ChangesetDetailsContext‘.. which is an internal class
  • Shelveset Details
    • Same as with Changesets – the context object is actually an instance of the internal type ‘ShelvesetDetailsContext‘ and therefore cannot be instantiated without Reflection.
  • Find Shelveset
    • .. and again – same for finding Shelvesets – the internal class used here is of type ‘FindShelvesetsModelContext’

 

I haven’t covered Git (commits, branches etc) here, yet – this might come at a later point.

What’s also worth mentioning, is that I’ve created a couple convenience wrappers for all these and other Team Explorer page navigations in my ‘JB.Common.VisualStudio.TeamFoundation‘ NuGet package (you might want to get the pre-release one), particularly as ITeamExplorer extension methods.

Team Foundation Server

VSTS & TFS Rest API: 05 – Teams

While we are still quite top-level and just recently covered VSTS / TFS Project Collections and Projects, it’s worth taking a brief, initial detour towards Teams.
It’ll be brief only, because this post will only cover retrieving and deleting existing Teams but also creating new ones programmatically using the API – the Work related topics (Team assets, capacity and activity planning etc) will be covered in a Work-specific post later-on.

Teams in VSTS & TFS are exactly what the name suggests – a core concept and structural entity that groups users into one or more Team(s), typically each Team working on a specific Feature at a time and each user may be part of more than one team. Each Team can get and typically works with a focused view on their planned work and its progress but from a more global, aggregate bird’s-eye view of the overall product’s or project’s progress across multiple teams is also easily available.

However, as this is an introduction into the APIs for VSTS & TFS’s features, we’ll start by retrieving existing Team(s) of a given Project as this is where they are located. What you’ll need is

  • The URL of your Project Collection (see the previous post)
  • The Name or Id of the Project for which you want to retrieve (and later create and delete) Teams

Much like all other posts, the client library comes with a specific class that provides access to Teams-specific functionality, in this case it’s the TeamHttpClient one.

Retrieving existing Teams

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

// Get a Team client
var teamHttpClient = visualStudioServicesConnection.GetClient&lt;TeamHttpClient&gt;();

// Retrieve existing Team(s)
// ##############################
// First lets retrieve the teams (first 10 again) for the given Project(Id)
var allTeams = teamHttpClient.GetTeamsAsync(ProjectId, 10, 0).Result;
foreach (var team in allTeams)
{
 Console.WriteLine("Team '{0}' (Id: {1})", team.Name, team.Id);
}

Retrieving Team Members

Besides retrieving the Teams, you can also retrieve the members of each team via the .GetTeamMembersAsync() method, i.e. like this:

// Retrieve Team Members
// ##############################
Console.WriteLine("Team with Id '{0}' contains the following member(s)", TeamIdKnownToContainMembers);
foreach (var identityReference in teamHttpClient.GetTeamMembersAsync(ProjectId, TeamIdKnownToContainMembers, 10, 0) .Result)
{
    Console.WriteLine("-- '{0}' (Id: {1})", identityReference.DisplayName, identityReference.Id);
}

Creating new Teams

Creaing new Teams is easily doable via the API, too, you’ll only need to provide the Name of the new Team and call the .CreateTeamAsync(..) method as shown below. Unlike other such activities, this one is not a queued job which we need to track its (completion or failure) progress for:

// Create Team(s)
// ##############################
var somewhatRandomValueForTeamName = (int)(DateTime.UtcNow - DateTime.UtcNow.Date).TotalSeconds;

// We can also create new Team(s), the minimum amount of information you have to provide is
var newTeam = new WebApiTeam()
{
 // .. only the .Name of the new Team
 // albeit it may NOT contain these characters: @ ~ ; ' + = , < > | / \ ? : & $ * " # [ ]
 // but i.e. whitespaces are just fine
 Name = $"My new Team {somewhatRandomValueForTeamName}"
};

// once we've prepared the team instance, we call the api endpoint using the teamHttpClient
var newlyCreatedTeam = teamHttpClient.CreateTeamAsync(newTeam, ProjectId).Result;

Console.WriteLine("Team '{0}' (Id: {1}) created", newlyCreatedTeam.Name, newlyCreatedTeam.Id);

Please note that new Teams are empty by default and adding members to that new Team seems to be.. well-hidden in the client at the moment – at least I couldn’t find it after digging around for a little while. It may not be (publicly) available in the API and REST endpoints just yet because the REST endpoint documentation also lacks any explanation if it’s possible or how to do it.
I’ll update this post whenever the functionality becomes available.

However, you can obviously manage Team members manually following the guidelines for VSTS & TFS.

Deleting Teams

Whenever you deem necessary, you can also delete Teams programmatically, i.e. like this:

// Delete Team(s)
// ##############################
// we can als delete existing Teams, i.e. the one we've just created
teamHttpClient.DeleteTeamAsync(ProjectId, newlyCreatedTeam.Id.ToString()).Wait();

Conclusion

Programmatically accessing and creating/deleting Team(s) is quite easy, you can similarly also update existing Team(s) via the .UpdateTeamAsync() method, but that only allows update(s) for .Name and .Description (not as mentioned above its Members).

The Work related functionality of VSTS and TFS which is closely tied to Teams will be covered in a later post, so stay tuned.

 

Index for all Posts | Full code is available at GitHub

Team Foundation Server

VSTS & TFS Rest API: 04 – Project Collections and Projects

After having successfully authenticated against your VSTS or TFS system, one of the first artifacts you’ll probably work with and access are Project Collections and their Projects. As you need the first to access the later, we’ll start with..

Project Collections

In order to access your Project Collections you’ll only need the base uri of your VSTS account or TFS system, which typically look like this:

  • VSTS: https://myaccount.visualstudio.com (replace ‘myaccount’ with your VSTS’s account name obviously)
  • TFS: http(s)://hostname:port/tfs (by default TFS installs and runs, if chosen, non-https on port 8080, or https on port 443)

 

This base uri is used for the VssConnection and you’ll be using a ProjectCollectionHttpClient in order to interact with your project collections:

using System;
using Microsoft.TeamFoundation.Core.WebApi;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;

namespace ProjectCollectionsAndProjects
{
    public class Program
    {
        const string BaseUri = @"http://win2012:8080/tfs/"; // In my case this is a locally running development TFS (2015 Update 2) system

        static void Main(string[] args)
        {
            // instantiate a vss connection using the BaseUri (not the Project Collection Uri!)
            var visualStudioServicesConnection = new VssConnection(new Uri(BaseUri), new VssCredentials());

            // get ahold of the Project Collection client
            var projectCollectionHttpClient = visualStudioServicesConnection.GetClient<ProjectCollectionHttpClient>();
            
            // iterate over the first 10 Project Collections (I am allowed to see)
            // however, if no parameter(s) were provided to the .GetProjectCollections() method, it would only retrieve one Collection,
            // so basically this allows / provides fine-grained pagination control
            foreach (var projectCollectionReference in projectCollectionHttpClient.GetProjectCollections(10, 0).Result)
            {
                // retrieve a reference to the actual project collection based on its (reference) .Id
                var projectCollection = projectCollectionHttpClient.GetProjectCollection(projectCollectionReference.Id.ToString()).Result;

                // the 'web' Url is the one for the PC itself, the API endpoint one is different, see below
                var webUrlForProjectCollection = projectCollection.Links.Links["web"] as ReferenceLink;

                Console.WriteLine("Project Collection '{0}' (Id: {1}) at Web Url: '{2}' & API Url: '{3}'",
                    projectCollection.Name,
                    projectCollection.Id,
                    webUrlForProjectCollection.Href,
                    projectCollection.Url);
            }
        }
    }
}

The only really interesting things to note here is that

  • the call to projectCollectionHttpClient.GetProjectCollections(10, 0) uses pagination and if you don’t specify any page size and starting page at all, it’ll only retrieve one result by default
  • and what you’ll get back from that call is a TeamProjectCollectionReference, which can / needs to be used to retrieve the actual Project Collection

and the most relevant part here to go on is the Project Collection’s URL, which will, in case of VSTS, looks like this: https://fabrikam.visualstudio.com/DefaultCollection/

Please note, the concept of Collections is currently undergoing a change towards ‘Organizations’ and the corresponding URLs you’ll see in VSTS and later on in TFS will probably no longer include the name of the Project Collection, but working with them via the API does require them, at least for the current API version.

 

Anyway, now that we have the URL for our Project Collection.. or well – if we had known it upfront, we could have just skipped that part in order to work with..

Projects

Retrieving Existing Projects

A Project collection may contain 0 or more Projects and in order to retrieve those, you’ll need to know the Project Collection’s base url and work with a ProjectHttpClient, i.e. like this:

// first we need to create a new connection based on the current Projec Collections 'web' Url
var projectVssConnection = new VssConnection(new Uri(urlForProjectCollection), new VssCredentials());

// and retrieve the corresponding project client 
var projectHttpClient = projectVssConnection.GetClient<ProjectHttpClient>();

// then - same as above.. iterate over the project references (with a hard-coded pagination of the first 10 entries only)
foreach (var projectReference in projectHttpClient.GetProjects(top: 10, skip: 0).Result)
{
    // and then get ahold of the actual project
    var teamProject = projectHttpClient.GetProject(projectReference.Id.ToString()).Result;
    var urlForTeamProject = ((ReferenceLink)teamProject.Links.Links["web"]).Href;

    Console.WriteLine("Team Project '{0}' (Id: {1}) at Web Url: '{2}' & API Url: '{3}'",
        teamProject.Name,
        teamProject.Id,
        urlForTeamProject,
        teamProject.Url);
}

or.. if you know the (Gu)Id of a given project, you can retrieve it directly like this:

// or if we already have the project collection url and the project's Id (Guid) upfront, we can access the later directly via:
var knownProjectCollectionUrl = @"http://win2012:8080/tfs/DefaultCollection/";
var projectVssConnectionForKnownProjectCollection = new VssConnection(new Uri(knownProjectCollectionUrl), new VssCredentials());
var projectHttpClientForKnownProjectCollection = projectVssConnectionForKnownProjectCollection.GetClient<ProjectHttpClient>();
var knownTeamProject = projectHttpClientForKnownProjectCollection.GetProject("dc68c474-2ce0-4be6-9617-abe97f66ec1e").Result;

What’s interesting to note here is that the .GetProject(s) methods take an optional (boolean) parameter named ‘includeCapabilities’ which by default is set to false, but if you do retrieve the projects’ capabilities, you can access them via the project’s .Capabilities property (a Dictionary of Dictionaries) like this:

var knownTeamProject = projectHttpClientForKnownProjectCollection.GetProject("dc68c474-2ce0-4be6-9617-abe97f66ec1e", includeCapabilities: true).Result;

// check whether Project uses Git or TFS Version Control
if (knownTeamProject.Capabilities.ContainsKey("versioncontrol") && knownTeamProject.Capabilities["versioncontrol"].ContainsKey("sourceControlType"))
{
    Console.WriteLine("{0} is used!", knownTeamProject.Capabilities["versioncontrol"]["sourceControlType"]);
}

// 'processTemplate' is apparently only an existing capability IF you use the standard templates (Agile, Scrum, CMMI)
// and /or your custom Process Template provides proper Process Configurations (which earlier versions of TFS did not have)
if (knownTeamProject.Capabilities.ContainsKey("processTemplate"))
{
    Console.WriteLine("Process Template '{0}' (Id: '{1}') is used!",
        knownTeamProject.Capabilities["processTemplate"]["templateName"],
        knownTeamProject.Capabilities["processTemplate"]["templateTypeId"]);
}

Finally, you can also get the TeamProject’s default Team via the .DefaultTean property, more precisely like this:

var knownTeamProject = projectHttpClientForKnownProjectCollection.GetProject("dc68c474-2ce0-4be6-9617-abe97f66ec1e", includeCapabilities: true).Result;

Console.WriteLine("Default Team is: '{0}' (Id: {1})", knownTeamProject.DefaultTeam.Name, knownTeamProject.DefaultTeam.Id);

More details about and working with Teams will follow in a later post, too.

Creating new Projects

While retrieving Team Projects might be a common task, you can also create new Projects via the API (assuming you have the necessary permissions, i.e. Project Collection Administrator), particularly via the .QueueCreateProject(…) method. This method takes a newly constructed TeamProject instance and, as the name implies, queues the Team Project creation in the Project Collection and you can track the status of the queued creation work via a OperationsHttpClient:

// We can also create new projects, i.e. like this:
var newTeamProjectToCreate = new TeamProject();
var somewhatRandomValueForProjectName = (int)(DateTime.UtcNow - DateTime.UtcNow.Date).TotalSeconds;

// mandatory information is name,
newTeamProjectToCreate.Name = $"Dummy Project {somewhatRandomValueForProjectName}";

// .. description
newTeamProjectToCreate.Description = $"This is a dummy project";

// and capabilities need to be provided
newTeamProjectToCreate.Capabilities = new Dictionary<string, Dictionary<string, string>>
{
    {
        // particularly which version control the project shall use (as of writing 'TFVC' and 'Git' are available
        "versioncontrol", new Dictionary<string, string>()
        {
            {"sourceControlType", "TFVC"}
        }
    },
    {
        // and which Process Template to use
        "processTemplate", new Dictionary<string, string>()
        {
            {"templateTypeId", "adcc42ab-9882-485e-a3ed-7678f01f66bc"} // This is the Id for the Agile template, on my TFS server at least.
        }
    }
};

// because project creation takes some time on the server, the creation is queued and you'll get back a 
// ticket / reference to the operation which you can use to track the progress and/or completion
var operationReference = projectHttpClientForKnownProjectCollection.QueueCreateProject(newTeamProjectToCreate).Result;

Console.WriteLine("Project '{0}' creation is '{1}'", newTeamProjectToCreate.Name, operationReference.Status);

// tracking the status via a OperationsHttpClient (for the Project collection
var operationsHttpClientForKnownProjectCollection = projectVssConnectionForKnownProjectCollection.GetClient<OperationsHttpClient>();

var projectCreationOperation = operationsHttpClientForKnownProjectCollection.GetOperation(operationReference.Id).Result;
while (projectCreationOperation.Status != OperationStatus.Succeeded
    && projectCreationOperation.Status != OperationStatus.Failed
    && projectCreationOperation.Status != OperationStatus.Cancelled)
{
    Console.Write(".");
    Thread.Sleep(1000); // yuck

    projectCreationOperation = operationsHttpClientForKnownProjectCollection.GetOperation(operationReference.Id).Result;
}

// alright - creation is finished, successfully or not
Console.WriteLine("Project '{0}' creation finished with State '{1}' & Message: '{2}'",
    newTeamProjectToCreate.Name,
    projectCreationOperation.Status,
    projectCreationOperation.ResultMessage ?? "n.a.");

Updating Projects

You can also update existing Projects, particularly their Description but also Name using the .UpdateProject() method like this:

newTeamProjectJustCreated.Description = "Some updated Description";

var projectUpdateOperationReference = projectHttpClientForKnownProjectCollection.UpdateProject(newTeamProjectJustCreated.Id, newTeamProjectJustCreated).Result; // .Id stays the same, even after updates

Console.WriteLine("Project '{0}' Update is '{1}'", newTeamProjectToCreate.Name, projectUpdateOperationReference.Status);

// and again, we track the queued deletion work / operation like before
var projectUpdateOperation = operationsHttpClientForKnownProjectCollection.GetOperation(projectUpdateOperationReference.Id).Result;
while (projectUpdateOperation.Status != OperationStatus.Succeeded
    && projectUpdateOperation.Status != OperationStatus.Failed
    && projectUpdateOperation.Status != OperationStatus.Cancelled)
{
    Console.Write(".");
    Thread.Sleep(1000); // again, yuck

    projectUpdateOperation = operationsHttpClientForKnownProjectCollection.GetOperation(projectUpdateOperationReference.Id).Result;
}

Console.WriteLine("Project '{0}' Update finished with State '{1}' & Message: '{2}'",
    newTeamProjectJustCreated.Name,
    projectUpdateOperation.Status,
    projectUpdateOperation.ResultMessage ?? "n.a.");

Deleting Projects

Similarly you can also delete previously created Team Projects via the API, more precisely via the .QueueDeleteProject() method:

var projectDeletionOperationReference = projectHttpClientForKnownProjectCollection.QueueDeleteProject(projectGuid).Result;

Console.WriteLine("Project '{0}' Deletion is '{1}'", newTeamProjectToCreate.Name, projectCreationOperationReference.Status);

// and again, we track the queued deletion work / operation like before
var projectDeletionOperation = operationsHttpClientForKnownProjectCollection.GetOperation(projectDeletionOperationReference.Id).Result;
while (projectDeletionOperation.Status != OperationStatus.Succeeded
    && projectDeletionOperation.Status != OperationStatus.Failed
    && projectDeletionOperation.Status != OperationStatus.Cancelled)
{
    Console.Write(".");
    Thread.Sleep(1000); // again, yuck

    projectDeletionOperation = operationsHttpClientForKnownProjectCollection.GetOperation(projectDeletionOperationReference.Id).Result;
}

// alright - creation is finished, successfully or not
Console.WriteLine("Project '{0}' Deletion finished with State '{1}' & Message: '{2}'",
    newTeamProjectToCreate.Name,
    projectCreationOperation.Status,
    projectCreationOperation.ResultMessage ?? "n.a.");

Conclusion

That’s it for the Project Collections and Projects currently – there’s one method for Projects left not covered here (.GetProjectHistory()) which does exactly what the name states – gets all historic revision of a Team Project, i.e. to see what changed over time (Description, Name etc). However, that’s a rather rare use-case and follows the rest of the methods and APIs shown above, so its for you to find out yourself.

 

Index for all Posts | Full code is available at GitHub

Team Foundation Server

VSTS & TFS Rest API: 03 – Authentication

As mentioned in the previous post, there are several ways to authenticate yourself against your target VSTS or TFS endpoint and depending on your environment, you will have to use one or the other.

When constructing a new VssConnection instance, you have to pass along a VssCredentials instance into it, the later being, as the name implies, the container for your authentication credentials and it comes in various flavours (read: .ctor overloads and derived classes).

NTLM

The most basic one is constructing a VssCredentials instance with no parameter at all and what you’ll be using is simply put integrated authentication / NTLM:

var visualStudioServicesConnection = new VssConnection(new Uri(baseUri), new VssCredentials());

Basic Authentication

VSTS and TFS also provide means to utilize Basic authentication (HTTP AUTH) which you need to create and enable first (see VSTS guidelines) and once you’ve done so, you can use them via the API like this:

var visualStudioServicesConnection = new VssConnection(new Uri(baseUri), new VssBasicCredential(username, password));

Personal Access Tokens

Next up are Personal Access Tokens (PAT) which you can easily create following the VSTS guidelines and those PATs are a means of authenticating separately from your actual credentials with a fine-grained & per access token scopes of security. Simply put it allows you to create a PAT for every use-case or even application and thereby enabling a secure and clearly separated way of giving an application or 3rd party access to your VSTS or TFS system on your behalf.

To use these via the API, you use the exact same mechanism as via Basic Authentication but you simply don’t provide any username (well – an empty one to be precise), and the PAT itself is used as the password:

var visualStudioServicesConnection = new VssConnection(new Uri(baseUri), new VssBasicCredential(string.Empty, pat));

Visual Studio Sign-in Prompt

Moreover another way of authenticating is using the standard VS Sign-In prompt which is similarly easy and exposed via the VssClientCredentials class:

var visualStudioServicesConnection = new VssConnection(new Uri(baseUri), new VssClientCredentials());

OAuth Authentication

OAuth is a widely used but a slightly more tedious authorization protocol to implement but luckily there’s a thorough sample application available at CodePlex specifically for VSTS / VSO (which also works for on-premises).

Once you have the corresponding access token, you can use it to VSTS / TFS utilizing the VssOAuthCredential class:

var visualStudioServicesConnection = new VssConnection(new Uri(baseUri), new VssOAuthCredential(accessToken));

Azure Active Directory Authentication

Last but not least you can utilize Azure Active Directory identities to authenticate against a VSTS or TFS system via the VssAadCredential class:

var visualStudioServicesConnection = new VssConnection(new Uri(baseUri), new VssAadCredential(username, password));

Conclusion

Those are the authentication types currently available (or at least the ones I know of) and most are quite easily usable, just make sure to keep the sensitive bits (i.e. your Personal Access Token or OAuth Access Token) secure.

 

Index for all Posts | Full code is available at GitHub

Team Foundation Server

VSTS & TFS Rest API: 02 – Basics

As this series of posts will be using the (new) .Net client libraries, the first thing we’ll be doing is adding a reference to the core NuGet package to our new project:

PM> Install-Package Microsoft.TeamFoundationServer.Client

as well as the Visual Studio Services client package:

PM> Install-Package Microsoft.VisualStudio.Services.InteractiveClient

What’s maybe a bit odd at first is that the Microsoft.VisualStudio.Services.InteractiveClient NuGet package contains and references a ‘Microsoft.VisualStudio.Services.Client.dll‘ assembly, while the ‘Microsoft.VisualStudio.Services.Client‘ NuGet package, even though named like the aforementioned assembly, contains *.Common assemblies only (basically the core contracts).

So even though the name and description implies user interaction, the ‘Microsoft.VisualStudio.Services.InteractiveClient‘ contains the interesting and useful bits, even without actual user interaction.

This will pull in a couple dependencies the packages have and reference about a dozen Microsoft.TeamFoundation.* and Microsoft.VisualStudio.* assemblies.

Now in order to make your first connection and actually retrieve data from your TFS Server, you will need the following information:

  • URL of the target Team Project Collection
  • Identifier for the artifact that you want to retrieve, i.e. a work item id

And the entire code to retrieve the later (work item) by its .Id looks like this:

using System;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;

namespace _02_Basics
{
    class Program
    {
        const string TeamProjectCollectionUri = @"http://win2012:8080/tfs/DefaultCollection"; // In my case this is a locally running development TFS (2015 Update 2) system
        const int WorkItemIdentifier = 1; // This is the Work Item .Id which we will be retrieving

        static void Main(string[] args)
        {
            var visualStudioServicesConnection = new VssConnection(new Uri(TeamProjectCollectionUri), new VssCredentials());
            var workItemTrackingHttpClient = visualStudioServicesConnection.GetClient&lt;WorkItemTrackingHttpClient&gt;();
            var workItemInstance = workItemTrackingHttpClient.GetWorkItemAsync(1).Result;

            Console.WriteLine("Work Item {0} - '{1}' retrieved", workItemInstance.Id, workItemInstance.Fields["System.Title"]);
        }
    }
}

These 3 lines basically make a connection (and in this case authenticate implicitely via NTLM, more about authentication will come later), get ahold of a WorkItemTrackingHttpClient and use the later to retrieve the Work Item with .Id 1 and then prints out some of its info the console.

There’s a wide variety of Client types you can retrieve from the VssConnection.GetClient<T>() method and the all derive from the VssHttpClientBase base class. We’ll go into all of these in later posts.

Moreover, some functionality may only work for the Microsoft hosted Visual Studio Team Services and not (yet) for on-premises Team Foundation Server installations as the release cycle of the later follows the former at a slower pace.

What’s also worth mentioning is that currently (v14.95.3 as of writing) the Types and Members exposed by the .Net Client library/libraries are somewhat inconsistently named (some have pre- and suffixes, some have abbreviations in their names, some do not) and their signatures appear to not follow a coherent guideline – some expose Task<List<T>>, some however return Task<IEnumerable<T>> (which seems a bit odd knowing that proper async enumerables aren’t there, yet), many methods do take an optional CancellationToken, many other methods don’t and it’s not clear why (the underlying base class(es) appear to boil down to helpers around standard HttpRequestMessage and HttpResponseMessage utilizing a normal HttpClient which works nicely with CancellationTokens.

As this may be confusing to some, I hope they’ll correct this somewhere down the road and provide a uniform and consistent library throughout and when they do, some of the sample code shown in this series of posts may no longer work as-is.

 

Index for all Posts | Full code is available at GitHub

Team Foundation Server

VSTS & TFS Rest API: 01 – Getting Started

First things first – this series of posts will be targeting the VSTS & TFS Rest API, as of writing version 1.0 is stable & some functionality is in preview state (see the API Change History), mostly using the .Net Client.

While VSTS is evolving quickly the on-premises Team Foundation Server itself is evolving at a slightly slower pace with its ~quarterly updates and some features being targeted at vNext and depending on your current environment, some functionality might not be available to you, yet.

So what you’ll need to follow along is either a VSTS account or a TFS installation and as this is code-focused, an installation of Visual Studio, Visual Studio Code, Xamarin Studio, JetBrains Rider or any other source code editor of your choice really.

Next up, you need to have a basic understanding of the underlying technologies in general, so it besides knowing the basics around TFS itself already, knowing a bit about HTTP, Rest and Json won’t hurt to understand what’s coming.

VSTS has a its own Getting Started page, which you might want to have a look at, too.

That’s basically all you need and we’re ready to dive into the actual API and coding against it.

 

Index for all Posts | Full code is available at GitHub