In Team Foundation Build every build definition is linked to a workspace, the workspace, like the one on your dev machine holds the source and manages files that are checked out. Sometimes you may want to check files in and out on the server during a build, for example when incrementing application or assembly version numbers, this is possible, however it requires the current workspace to be known. The only tfs activity available to get a workspace requires the name of the workspace to be known, tfs names the workspaces automatically so this isn't very helpful for a generic build process.

Creating a Custom Activity

If you haven't created a custom activity before there are many tutorials on the subject around, like this one, I won't reiterate the steps here. For this guide we will be creating an Activity and a CodeActivity.

First create a new class and add these namespaces at the top.


    using Microsoft.TeamFoundation.Build.Workflow.Activities;
    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.Build.Activities.Extensions;
    using Microsoft.TeamFoundation.VersionControl.Client;
    

These are contained in the files Microsoft.TeamFoundation.Build.Workflow.dll, Microsoft.TeamFoundation.Build.Activities.dll, Microsoft.TeamFoundation.Client.dll and Microsoft.TeamFoundation.VersionControl.Client.dll, which can generally be found in C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies\v2.0\

GetSourceWorkspace Activity

For our code activity to get access to an instance of VersionControlServer we need to pass in a TfsTeamProjectCollection object, the easiest way to do that is by using the built in GetTeamProjectCollection activity and passing the result in to our internal code activity as an argument.


    public sealed class GetSourceWorkspace : Activity<Workspace>
    {
        public GetSourceWorkspace()
        {
            this.Implementation = (() => CreateBody());
        }
        public Activity CreateBody()
        {
            GetSourceWorkspaceInternal getWorkspace = new GetSourceWorkspaceInternal();
            getWorkspace.TeamProjectCollection = new InArgument<TfsTeamProjectCollection>(new GetTeamProjectCollection());
            getWorkspace.Result = new OutArgument<Workspace>(x => this.Result.Get(x));
            return getWorkspace;
        }
    }
    

As you can see the activity merely creates an instance of our code activity (coming up next), assigns its result to the result of the outer activity and assigns the GetTeamProjectCollection activity as the argument for TeamProjectCollection.

GetSourceWorkspaceInternal Code Activity

The code activity is where most of the work happens, the basic idea of the activity is that it finds a file that exists in the current workspace and requests the workspace for that file.

We'll start by defining the class, we don't want the activity to be exposed directly so we will make it internal and we want it to return a Workspace object so we inherit CodeActivity<Workspace>. I do this in the same file as the above activity for brevity, however it is not required, if you add it to a separate file though you will need to move the namespace imports.


    internal sealed class GetSourceWorkspaceInternal : CodeActivity<Workspace>
    {
    }
    

We only need one InArgument for this activity to get the TeamProjectCollection


    public InArgument<TfsTeamProjectCollection> TeamProjectCollection { get; set; }
    

All of the work for the activity is done in the execute method, first off we can use the TeamProjectCollection we received from the outer activity to get the VersionControlServer service, this will be used to get the workspace.


    protected override Workspace Execute(CodeActivityContext context)
    {
        var teamProjectCollection = TeamProjectCollection.Get(context);
        var vcs = teamProjectCollection.GetService<VersionControlServer>();
    }
    

Next we need to find a file that is in our current workspace, the easy way to do this is to get the path of the sources directory and then find the first file in it, if this is done just after the build gets the source from tfs then the file will be part of the workspace. If this is done after a project has been built or any other temporary files are generated somehow then there is a chance that the first file found is not part of a workspace.


    var sourceDir = context.GetExtension<IEnvironmentVariableExtension>().GetEnvironmentVariable<String>(context, WellKnownEnvironmentVariables.SourcesDirectory);
    
    var mappedFile = Directory.EnumerateFiles(sourceDir, "*.*", SearchOption.AllDirectories).FirstOrDefault();
    
    if (mappedFile == null)
        throw new ArgumentException(String.Format("No files mapped in {0}, cannot get a workspace without any mapped directories", sourceDir));
    

Finally we need to use the path of the file we have found to get the workspace that contains it, first we update the workspace cache to ensure that our workspace request will use an updated list of workspaces, then we request the workspace from the VersionControlServer service.


    Workstation.Current.EnsureUpdateWorkspaceInfoCache(vcs, ".", new TimeSpan(0, 1, 0));
    var workspace = vcs.GetWorkspace(mappedFile);
    return workspace;
    

The final Execute method for copy/pasteability


    protected override Workspace Execute(CodeActivityContext context)
    {
        var teamProjectCollection = TeamProjectCollection.Get(context);
        var vcs = teamProjectCollection.GetService<VersionControlServer>();
        var sourceDir = context.GetExtension<IEnvironmentVariableExtension>().GetEnvironmentVariable<String>(context, WellKnownEnvironmentVariables.SourcesDirectory);
    
        var mappedFile = Directory.EnumerateFiles(sourceDir, "*.*", SearchOption.AllDirectories).FirstOrDefault();
        if (mappedFile == null)
            throw new ArgumentException(String.Format("No files mapped in {0}, cannot get a workspace without any mapped directories", sourceDir));
    
        Workstation.Current.EnsureUpdateWorkspaceInfoCache(vcs, ".", new TimeSpan(0, 1, 0));
        var workspace = vcs.GetWorkspace(mappedFile);
        return workspace;
    }
    

This can now be added into a build process like any other activity

Activity Example

In the next post I'll show how to use the workspace object to check in and check out files during a build.