PowerShell tips and tricks – Retrieving TFS collections and projects

Introduction

The following post is not really about a tip or a trick regarding a PowerShell itself. It’s more about on how to leverage some TFS libraries in order to automate processes regarding TFS via a PowerShell script. It will not show any fancy PowerShell technique but a way to query a TFS server of your choice, extract the necessary information and eventually make changes.

In this first tip we will see the essential, how to connect to the TFS, retrieve all of the available collections and list all of the projects for each TPC. Imagine that you are working on a TFS instance containing multiple collections, each one again having many projects (for many I do intend hundreds of projects in total). Verifying and changing settings on each one manually will not be easy nor convenient. As there is no UI for executing bulk operations on TFS, the easiest and most logical way to interact with it is through PowerShell. Let’s see how we can achieve that.

Prerequisites

Before we start, in order to be able to use these scripts you need to have at least PowerShell v3 installed and the necessary TFS libraries registered in GAC. The easiest way to make sure that you do have just mentioned libraries installed is to make sure that you do have Visual Studio installed on the machine you are executing this script from. Also it is good that the Visual Studio you added does match in version your TFS install. It means that if you do have TFS2013 installed, the best option will be to have Visual Studio 2013. For what concerns PowerShell, you do probably already have version 3 installed on your machine. The easiest way to check is to execute the following command:

$PSVersionTable.PSVersion

If is not the case that you do have PowerShell version 3 installed, you can do so by following this link: Windows Management Framework 3.0

In order to perform the call to the TFS server you need to have sufficient rights to do so. Managing collection objects require Edit instance-level information permission level, which is granted by default only to TFS admin. In case you do not have sufficient rights, you may encounter an exception reporting
Exception calling "GetCollections" with "0" argument(s): "Access Denied: Mario Majcica needs the following permission(s) to perform this action: Edit instance-level information"

Just for a sake of completeness I will add that this is not the only way of establishing a dialog with TFS, you could extract and set some of the information also through the REST API. Also some of the objects I do treat can be retrieved in a slightly more efficient way at the expense of simplicity.
In case you are interested about just mentioned techniques you can read the following post Building a TFS 2015 PowerShell Module using Nuget. Bare in mind that it is not targeting beginners as this blog post and it requires a deeper understanding on TFS Object Model and PowerShell.

Again, some utilities as Microsoft Visual Studio Team Foundation Server 2013 Power Tools do deliver PowerShell Cmdlets that can be used to work with different features of TFS such as changesets, shelvesets, workspaces and more. If interested in details about this approach I can advise you to Google out the argument or get your hands of a book called Windows PowerShell 4.0 for .NET Developers.

Let’s script

First things first. In order to reference the necessary libraries we need to issue the following command.

Add-Type -AssemblyName "Microsoft.TeamFoundation.Client, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

This may seem a quite complicated way to load an assembly, unfortunately only a certain pre-defined set of assemblies can be loaded by their partial name. For all the rest a fully qualified name is necessary. In case you decide to use some other TFS functionalities, it may be necessary to reference other libraries.

Note, in case of Visual Studio 2015, the object model client libraries are removed from GAC. However the necesseray libraries are still available. In order to load them you need to approach the load in a different manner.

Add-Type -Path "C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDECommonExtensionsMicrosoftTeamFoundationTeam ExplorerMicrosoft.TeamFoundation.Client.dll"

You need to point the Add-Type cmdlet to a path. It may vary based on the folder in which your Visual Studio is installed.

There is another option that may be a nice way to comply to this dependency. If you are an early adopter of PowerShell 5, you may retrieve the necessary package via OneGet.

Walking through TFS objects

All TFS libraries do have a same entry point. There are multiple factory classes, exposing static methods, that will give us the instance of classes implementing the requested interface thought whom we will perform desired operations. For a less experienced DevOps (at least less Dev’s more Ops) this may not make sense, thus let me try to explain it a bit better.
In order to use an object (a non-static class in this particular case) we need to construct an instance of it (and probably pass some parameters for its initialization). Via the factory pattern, adopted by Microsoft for this particular set of libraries, we are able to get the right instance of the class implementing the interface we are in need for. Still too complicated? Then I’m sorry, I’m already going way out of scope here.

In order to get the object through whom we will get the services we are in need for, we need to call a static method GetConfigurationServer on TfsConfigurationServerFactory class.
This call will return an instance of ConfigurationServer class which will be our main entry point for all the services.

$uri = "http://my.tfs.local:8080/tfs"
$tfsConfigurationServer = [Microsoft.TeamFoundation.Client.TfsConfigurationServerFactory]::GetConfigurationServer($uri)

As you can see I’m passing to the GetConfigurationServer method a parameter, which is the address of our TFS. It means that all of the operations performed through its service calls will be pointing to TFS indicated in this address.

Once our entry point is obtained, we can ask him to get us an instance of the class that implements the necessary logic to perform actions we are in need for.
In our case this is a class that implements ITeamProjectCollectionService interface.
We can request it with the following command

$tpcService = $tfsConfigurationServer.GetService("Microsoft.TeamFoundation.Framework.Client.ITeamProjectCollectionService")

Now that I do have a correct instance of the class that implements the interface which declares the methods I’m interested in, I will just call a method that will return a collection of TeamProjectCollection objects.
This method is called GetCollections and it is invoked as follows.

$tpcService.GetCollections() 

I’m now able to iterate through this collection and retrieve, beside others, the name of each project collection. Each object in the collection represents a project collection on our TFS. There should be always at least one element in it.

Let’s recap our script before continuing.

Add-Type -AssemblyName "Microsoft.TeamFoundation.Client, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

$uri = "http://tfs:8080/tfs"
$tfsConfigurationServer = [Microsoft.TeamFoundation.Client.TfsConfigurationServerFactory]::GetConfigurationServer($uri)
$tpcService = $tfsConfigurationServer.GetService("Microsoft.TeamFoundation.Framework.Client.ITeamProjectCollectionService")

$sortedCollections = $tpcService.GetCollections() | Sort-Object -Property Name

foreach($collection in $sortedCollections) {
    Write-Host $collection.Name
}

The only small detail I haven’t mentioned until now, is the fact that once I do retrieve the collections I do sort them base on the value of Name property.

Now that we have a list of all TPC’s we can construct a URL that will be used to get other services which as a starting point do require a reference to the TPC.
As just mentioned in order to construct the URL I will declare another variable and concatenate the TFS URL and the project name after which I can request an instance of TfsTeamProjectCollectionFactory class that I will use as the entry point for all the operations on the given TPC.

As already seen for the ITeamProjectCollectionService we need to obtain a service that will provide us with the necessary data. In our case this is ICommonStructureService3. It is all achieved by the following code.

Now we are able to invoke a method called ListProjects in order to get all of the projects part of that TPC.

$collectionUri = $uri + "/" + $collection.Name
$tfsTeamProject = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($collectionUri)
$cssService = $tfsTeamProject.GetService("Microsoft.TeamFoundation.Server.ICommonStructureService3")   
$sortedProjects = $cssService.ListProjects() | Sort-Object -Property Name

Each value in $sortedProjects will be of type ProjectInfo and within we will find all of the necessary information about that Team Project.
In between other properties and methods, as expected, we do have a property called Name. We will output the name of all the projects in that TCP.

The end result

I will add some of the formatting for our messages so that the output of the script is easier to read. Also I will collect the total number of projects. This code I do hope is not needed to be explained in detail. Following the complete script.

Add-Type -AssemblyName "Microsoft.TeamFoundation.Client, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

$uri = "http://tfs:8080/tfs"
$tfsConfigurationServer = [Microsoft.TeamFoundation.Client.TfsConfigurationServerFactory]::GetConfigurationServer($uri)
$tpcService = $tfsConfigurationServer.GetService("Microsoft.TeamFoundation.Framework.Client.ITeamProjectCollectionService")

$sortedCollections = $tpcService.GetCollections() | Sort-Object -Property Name
$numberOfProjects = 0

foreach($collection in $sortedCollections) {
    $collectionUri = $uri + "/" + $collection.Name
    $tfsTeamProject = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($collectionUri)
    $cssService = $tfsTeamProject.GetService("Microsoft.TeamFoundation.Server.ICommonStructureService3")   
    $sortedProjects = $cssService.ListProjects() | Sort-Object -Property Name

    Write-Host $collection.Name "- contains" $sortedProjects.Count "project(s)"

    foreach($project in $sortedProjects)
    {
        $numberOfProjects++
        Write-Host (" - " + $project.Name)
    }
}

Write-Host
Write-Host "Total number of project collections" $sortedCollections.Count
Write-Host "Total number of projects           " $numberOfProjects

As there are plenty of properties available, your are able to change or check a setting on all of the projects in all of the TCP’s you have. This can be very handy for the maintenance task as we are going to see later in a blog post that I’m currently working on.

Happy coding!

Available modules for TFS 2015 build tasks

It is not yet well documented but if you are writing a custom build task for your TFS 2015 build system, you get at your disposition some of the modules that are made available by your VsoWorker.
As I couldn’t find a list of available modules and cmdlets they expose, I decided to dig into them and see what is there. I also wanted to check if with the Update 1 (and the new version of the build agent) there will be more of them.
I wrote a handy task that will list all of the available cmdlets for all of the modules shipped by the agent worker. Following are my results.

With TFS 2015 the agent version delivered is 1.83.2. It offers the following modules:

  • Microsoft.TeamFoundation.DistributedTask.Task.Common.dll
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Azure.psm1
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Chef.psm1
  • Microsoft.TeamFoundation.DistributedTask.Task.DevTestLabs.dll
  • Microsoft.TeamFoundation.DistributedTask.Task.DTA.dll
  • Microsoft.TeamFoundation.DistributedTask.Task.Internal.dll
  • Microsoft.TeamFoundation.DistributedTask.Task.TestResults.dll

I will now list of all importable functions and cmdlets in those modules.

  • Microsoft.TeamFoundation.DistributedTask.Task.Common.dll
    • Add-TaskIssue
    • Complete-Task
    • Find-Files
    • Get-LocalizedString
    • Set-TaskProgress
    • Set-TaskVariable
    • Write-TaskDetail
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Azure.psm1
    • Get-AzureCmdletsVersion
    • Get-AzureModuleLocation
    • Get-AzureVersionComparison
    • Get-RequiresEnvironmentParameter
    • Get-SelectNotRequiringDefault
    • Import-AzurePowerShellModule
    • Initialize-AzurePowerShellSupport
    • Initialize-AzureSubscription
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Chef.psm1
    • Get-DetailedRunHistory
    • Get-PathToNewtonsoftBinary
    • Get-ShouldWaitForNodeRuns
    • Get-TemporaryDirectoryForChef
    • Initialize-ChefRepo
    • Invoke-GenericMethod
    • Invoke-Knife
    • Invoke-WithRetry
    • Wait-ForChefNodeRunsToComplete
  • Microsoft.TeamFoundation.DistributedTask.Task.DevTestLabs.dll
    • Complete-EnvironmentOperation
    • Complete-EnvironmentResourceOperation
    • Complete-ResourceOperation
    • Copy-FilesToAzureBlob
    • Copy-FilesToRemote
    • Copy-FilesToTargetMachine
    • Copy-ToAzureMachines
    • Get-Environment
    • Get-EnvironmentProperty
    • Get-EnvironmentResources
    • Get-ProviderData
    • Invoke-BlockEnvironment
    • Invoke-EnvironmentOperation
    • Invoke-PsOnRemote
    • Invoke-ResourceOperation
    • Invoke-UnblockEnvironment
    • New-OperationLog
    • Register-Environment
    • Register-EnvironmentDefinition
    • Register-Provider
    • Register-ProviderData
    • Remove-Environment
    • Remove-EnvironmentResources
  • Microsoft.TeamFoundation.DistributedTask.Task.DTA.dll
    • Invoke-DeployTestAgent
    • Invoke-RunDistributedTests
  • Microsoft.TeamFoundation.DistributedTask.Task.Internal.dll
    • Add-BuildArtifactLink
    • Add-BuildAttachment
    • Convert-String
    • Copy-BuildArtifact
    • Get-JavaDevelopmentKitPath
    • Get-MSBuildLocation
    • Get-ServiceEndpoint
    • Get-TaskVariable
    • Get-ToolPath
    • Get-VisualStudioPath
    • Get-VssConnection
    • Get-X509Certificate
    • Invoke-Ant
    • Invoke-BatchScript
    • Invoke-IndexSources
    • Invoke-Maven
    • Invoke-MSBuild
    • Invoke-PublishSymbols
    • Invoke-Tool
    • Invoke-VSTest
    • Publish-BuildArtifact
    • Register-XamarinLicense
    • Unregister-XamarinLicense
  • Microsoft.TeamFoundation.DistributedTask.Task.TestResults.dll
    • Invoke-ResultPublisher
    • Publish-TestResults

The Update 1 RC1 for TFS increased the agent to the version 1.89.0 and the RC2 incremented it to version 1.89.1. As of the time I’m writing this blog post, the RC2 is the latest version available, I’ll list and show the differences with a plain TFS 2015 build agent.

Three new modules are added:

  • Microsoft.TeamFoundation.DistributedTask.Task.CodeCoverage
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Internal
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.RemoteDeployment

Following the list of all importable functions and cmdlets in the those modules.

  • Microsoft.TeamFoundation.DistributedTask.Task.CodeCoverage
    • Enable-CodeCoverage
    • Publish-CodeCoverage
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Internal
    • Get-OperationLogs
    • Get-ResourceCredentials
    • Get-ResourceFQDNTagKey
    • Get-ResourceHttpsTagKey
    • Get-ResourceHttpTagKey
    • Get-ResourceOperationLogs
    • Get-SkipCACheckTagKey
    • Get-SqlPackageCommandArguments
    • Import-DevtestLabsCommomDll
    • Write-ResponseLogs
  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.RemoteDeployment
    • Invoke-RemoteDeployment

Furthermore some cmdlets are added to the existing modules:

  • Microsoft.TeamFoundation.DistributedTask.Task.Deployment.Azure.psm1
    • Set-CurrentAzureRMSubscription
    • Set-CurrentAzureSubscription
  • Microsoft.TeamFoundation.DistributedTask.Task.DevTestLabs.dll
    • Get-ExternalIpAddress
    • Get-ParsedSessionVariables
  • Microsoft.TeamFoundation.DistributedTask.Task.Internal.dll
    • Get-IndexedSourceFilePaths
    • Get-TfsClientCredentials

It also seems that in the Microsoft.TeamFoundation.DistributedTask.Task.Internal.dll, Invoke-IndexSources cmdlet is not available any more.

You can reference these modules from your custom Build tasks by calling a standard import module cmdlet.

import-module "Microsoft.TeamFoundation.DistributedTask.Task.Common"

If you are interested about the agent version, you can check it by executing the vsoagent on your build server, with the following parameter:

VsoAgent.exe /version

As soon as an RTM version of the Update 1 becomes available, I’ll update this post.

In case you are interested in a short description of these functions, you can their source code for further info

CodedUI Tips and Tricks – Unavailable search attributes

Searching and filtering for the UITestControl objects is a straight forward process you can read about in plenty of places. All of them will tell you on how to leverage the SearchProperties and FilterProperties of the object in order to get the access to the desired control.

In both of these properties once you add the new PropertyExpression to the collection, you need to provide the property name. Each class that is based on HtmlControl base class, by contract, implements and exposes a property called PropertyNames. In the property implementation all of the controls do inherit from HtmlControl.PropertyNames which on it’s own brings the content of the class UITestControl.PropertyNames, and extend them with some control specific properties. This class is just a collection of strings, indicating the name of the element we are in search off. As this can sound confusional I’ll make a simple example of my typical control search:

HtmlButton okButton = new HtmlButton(browserWindow);
okButton.SearchProperties.Add(HtmlControl.PropertyNames.Id, "btnOk");
okButton.FilterProperties.Add(HtmlControl.PropertyNames.InnerText, "OK");

Now, no matter that the first parameter of the Add method is a string, and that PropertyNames collection is just that, a collection of strings, if the element you are searching for is not listed inside the PropertyNames collection and you try to set it by your custom constant like “style”, the control search will not be correctly performed.

popUpDiv.SearchProperties.Add("style", "display: block");

What is left for us to do in cases like this, is to explore the property called ControlDefinition. All of the attributes of the element we are exploring will be listed inside this property which is part of HtmlControl object. Luckily we can bring this property also inside the search and filtering criteria and we do not need to check the content of the controls in nasty loops and conditional operators.
In the following sample I am searching for my style setting and leveraging the PropertyExpressionOperator to imply a property value contains the given string.

popUpDiv.SearchProperties.Add(
    HtmlControl.PropertyNames.ControlDefinition, 
    "display: block", 
    PropertyExpressionOperator.Contains);

You also need to be sure on how is your element and it’s value written inside this property as it often has some custom way of picking these values up. The quickest way that works for me is to debug my automation code and check the content of this property.
What I usually do is, narrow the search as much as possible and then resolve all of the controls that match that criteria.

UITestControlCollection controls = popUpDiv.FindMatchingControls();

Once I found my control I do annotate the property value and use this information to setup my search or filtering.

I hope this tip saved you some time as this kind of things are not documented on MSDN nor are popular topic in the blogging community.

Happy coding!