Version Assemblies with TFS 2010 Continuous Integration

When I first heard that TFS 2010 had moved to Workflow Foundation for Team Build, I was *extremely* skeptical. I’ve loved MSBuild and didn’t quite understand the reasons for this change. In fact, given that I’ve been exclusively using Cruise Control for Continuous Integration (CI) for the last 5+ years of my career, I was skeptical of TFS for CI in general. However, after going through the learning process for TFS 2010 recently, I’m starting to become a believer. I’m also starting to see some of the benefits with Workflow Foundation for the overall processing because it gives you constructs not available in MSBuild such as parallel tasks, better control flow constructs, and a slightly better customization story.

The first customization I had to make to the build process was to version the assemblies of my solution. This is not new. In fact, I’d recommend reading Mike Fourie’s well known post on Versioning Code in TFS before you get started. This post describes several foundational aspects of versioning assemblies regardless of your version of TFS. The main points are: 1) don’t use source control operations for your version file, 2) use a schema like <Major>.<Minor>.<IncrementalNumber>.0, and 3) do not keep AssemblyVersion and AssemblyFileVersion in sync.

To do this in TFS 2010, the best post I’ve found has been Jim Lamb’s post of building a custom TFS 2010 workflow activity. Overall, this post is excellent but the primary issue I have with it is that the assembly version numbers produced are based in a date and look like this: “2010.5.15.1”. This is definitely not what I want. I want to be able to communicate to the developers and stakeholders that we are producing the “1.1 release” or “1.2 release” – which would have an assembly version number of “1.1.317.0” for example.

In this post, I’ll walk through the process of customizing the assembly version number based on this method – customizing the concepts in Lamb’s post to suit my needs. I’ll also be combining this with the concepts of Fourie’s post – particularly with regards to the standards around how to version the assemblies.

The first thing I’ll do is add a file called SolutionAssemblyVersionInfo.cs to the root of my solution that looks like this:

   1:  using System;
   2:  using System.Reflection;
   3:  [assembly: AssemblyVersion("")]
   4:  [assembly: AssemblyFileVersion("")]

I’ll then add that file as a Visual Studio link file to each project in my solution by right-clicking the project, “Add – Existing Item…” then when I click the SolutionAssemblyVersionInfo.cs file, making sure I “Add As Link”:

Now the Solution Explorer will show our file. We can see that it’s a “link” file because of the black arrow in the icon within all our projects.

sol explorer with ver. file

Of course you’ll need to remove the AssemblyVersion and AssemblyFileVersion attributes from the AssemblyInfo.cs files to avoid the duplicate attributes since they now leave in the SolutionAssemblyVersionInfo.cs file. This is an extremely common technique so that all the projects in our solution can be versioned as a unit.

At this point, we’re ready to write our custom activity. The primary consideration is that I want the developer and/or tech lead to be able to easily be in control of the Major.Minor and then I want the CI process to add the third number with a unique incremental number. We’ll leave the fourth position always “0” for now – it’s held in reserve in case the day ever comes where we need to do an emergency patch to Production based on a branched version.


Writing the Custom Workflow Activity

Similar to Lamb’s post, I’m going to write two custom workflow activities. The “outer” activity (a xaml activity) will be pretty straight forward. It will check if the solution version file exists in the solution root and, if so, delegate the replacement of version to the AssemblyVersionInfo activity which is a CodeActivity highlighted in red below:

version xaml


Notice that the arguments of this activity are the “solutionVersionFile” and “tfsBuildNumber” which will be passed in. The tfsBuildNumber passed in will look something like this: “CI_MyApplication.4” and we’ll need to grab the “4” (i.e., the incremental revision number) and put that in the third position. Then we’ll need to honor whatever was specified for Major.Minor in the SolutionAssemblyVersionInfo.cs file. For example, if the SolutionAssemblyVersionInfo.cs file had “” for the AssemblyVersion (as shown in the first code block near the beginning of this post), then we want to resulting file to have “”.

Before we do anything, let’s put together a unit test for all this so we can know if we get it right:

   1:  [TestMethod]
   2:  public void Assembly_version_should_be_parsed_correctly_from_build_name()
   3:  {
   4:      // arrange
   5:      const string versionFile = "SolutionAssemblyVersionInfo.cs";
   6:      WriteTestVersionFile(versionFile);
   7:      var activity = new VersionAssemblies();
   8:      var arguments = new Dictionary<string, object> { 
   9:                          { "tfsBuildNumber", "CI_MyApplication.4"},
  10:                          { "solutionVersionFile", versionFile}
  11:      };
  13:      // act
  14:      var result = WorkflowInvoker.Invoke(activity, arguments);
  16:      // assert
  17:      Assert.AreEqual("", (string)result["newAssemblyFileVersion"]);
  18:      var lines = File.ReadAllLines(versionFile);
  19:      Assert.IsTrue(lines.Contains("[assembly: AssemblyVersion(\"\")]"));
  20:      Assert.IsTrue(lines.Contains("[assembly: AssemblyFileVersion(\"\")]"));
  21:  }
  23:  private void WriteTestVersionFile(string versionFile)
  24:  {
  25:      var fileContents = "using System.Reflection;\n" +
  26:          "[assembly: AssemblyVersion(\"\")]\n" +
  27:          "[assembly: AssemblyFileVersion(\"\")]";
  28:      File.WriteAllText(versionFile, fileContents);
  29:  }


At this point, the code for our AssemblyVersion activity is pretty straight forward:

   1:  [BuildActivity(HostEnvironmentOption.Agent)]
   2:  public class AssemblyVersionInfo : CodeActivity
   3:  {
   4:      [RequiredArgument]
   5:      public InArgument<string> FileName { get; set; }
   7:      [RequiredArgument]
   8:      public InArgument<string> TfsBuildNumber { get; set; }
  10:      public OutArgument<string> NewAssemblyFileVersion { get; set; }
  12:      protected override void Execute(CodeActivityContext context)
  13:      {
  14:          var solutionVersionFile = this.FileName.Get(context);
  16:          // Ensure that the file is writeable
  17:          var fileAttributes = File.GetAttributes(solutionVersionFile);
  18:          File.SetAttributes(solutionVersionFile, fileAttributes & ~FileAttributes.ReadOnly);
  20:          // Prepare assembly versions
  21:          var majorMinor = GetAssemblyMajorMinorVersionBasedOnExisting(solutionVersionFile);
  22:          var newBuildNumber = GetNewBuildNumber(this.TfsBuildNumber.Get(context));
  23:          var newAssemblyVersion = string.Format("{0}.{1}.0.0", majorMinor.Item1, majorMinor.Item2);
  24:          var newAssemblyFileVersion = string.Format("{0}.{1}.{2}.0", majorMinor.Item1, majorMinor.Item2, newBuildNumber);
  25:          this.NewAssemblyFileVersion.Set(context, newAssemblyFileVersion);
  27:          // Perform the actual replacement
  28:          var contents = this.GetFileContents(newAssemblyVersion, newAssemblyFileVersion);
  29:          File.WriteAllText(solutionVersionFile, contents);
  31:          // Restore the file's original attributes
  32:          File.SetAttributes(solutionVersionFile, fileAttributes);
  33:      }
  35:      #region Private Methods
  37:      private string GetFileContents(string newAssemblyVersion, string newAssemblyFileVersion)
  38:      {
  39:          var cs = new StringBuilder();
  40:          cs.AppendLine("using System.Reflection;");
  41:          cs.AppendFormat("[assembly: AssemblyVersion(\"{0}\")]", newAssemblyVersion);
  42:          cs.AppendLine();
  43:          cs.AppendFormat("[assembly: AssemblyFileVersion(\"{0}\")]", newAssemblyFileVersion);
  44:          return cs.ToString();
  45:      }
  47:      private Tuple<string, string> GetAssemblyMajorMinorVersionBasedOnExisting(string filePath)
  48:      {
  49:          var lines = File.ReadAllLines(filePath);
  50:          var versionLine = lines.Where(x => x.Contains("AssemblyVersion")).FirstOrDefault();
  52:          if (versionLine == null)
  53:          {
  54:              throw new InvalidOperationException("File does not contain [assembly: AssemblyVersion] attribute");
  55:          }
  57:          return ExtractMajorMinor(versionLine);
  58:      }
  60:      private static Tuple<string, string> ExtractMajorMinor(string versionLine)
  61:      {
  62:          var firstQuote = versionLine.IndexOf('"') + 1;
  63:          var secondQuote = versionLine.IndexOf('"', firstQuote);
  64:          var version = versionLine.Substring(firstQuote, secondQuote - firstQuote);
  65:          var versionParts = version.Split('.');
  66:          return new Tuple<string, string>(versionParts[0], versionParts[1]);
  67:      }
  69:      private string GetNewBuildNumber(string buildName)
  70:      {
  71:          return buildName.Substring(buildName.LastIndexOf(".") + 1);
  72:      }
  74:      #endregion
  75:  }


At this point the final step is to incorporate this activity into the overall build template. Make a copy of the DefaultTempate.xaml – we’ll call it DefaultTemplateWithVersioning.xaml. Before the build and labeling happens, drag the VersionAssemblies activity in. Then set the LabelName variable to “BuildDetail.BuildDefinition.Name + "-" + newAssemblyFileVersion since the newAssemblyFileVersion was produced by our activity.

tfs version templ


Configuring CI

Once you add your solution to source control, you can configure CI with the build definition window as shown here. The main difference is that we’ll change the Process tab to reflect a different build number format and choose our custom build process file:

build process tab


When the build completes, we’ll see the name of our project with the unique revision number:

tfs completed builds


If we look at the detailed build log for the latest build, we’ll see the label being created with our custom task:

 tfs build log


We can now look at the history labels in TFS and see the project name with the labels (the Assignment activity I added to the workflow):

 tfs labels

Finally, if we look at the physical assemblies that are produced, we can right-click on any assembly in Windows Explorer and see the assembly version in its properties:

assembly props


Full Traceability

We now have full traceability for our code. There will never be a question of what code was deployed to Production. You can always see the assembly version in the properties of the physical assembly. That can be traced back to a label in TFS where the unique revision number matches. The label in TFS gives you the complete snapshot of the code in your source control repository at the time the code was built. This type of process for full traceability has been used for many years for CI – in fact, I’ve done similar things with CCNet and SVN for quite some time. This is simply the TFS implementation of that pattern. The new features that TFS 2010 give you to make these types of customizations in your build process are quite easy once you get over the initial curve.

posted on Saturday, May 15, 2010 8:00 AM Print
# re: Version Assemblies with TFS 2010 Continuous Integration
6/9/2010 9:23 AM
@roto - Sounds like you haven't re-built and re-deployed your DLL to the TFS server. Remember the AssemblyVersionInfo activity is C# that gets compiled into a DLL that needs to be re-deployed each time you make a change.
# re: Version Assemblies with TFS 2010 Continuous Integration
6/9/2010 12:47 PM
@roto - Did you add that argument to your xaml? You can see it in the 3rd screen shot above.
# re: Version Assemblies with TFS 2010 Continuous Integration
6/10/2010 1:31 PM
Hi Steve,

Just wondering if you can share the code and XAML for the custom workflow activity that you created in this post?

# re: Version Assemblies with TFS 2010 Continuous Integration
6/17/2010 8:50 AM
@Ash - The complete code for the AssemblyVersionInfo activity is shown above. The xaml that calls it is above as well. There is no other code in the solution other than those 2 things.
# re: Version Assemblies with TFS 2010 Continuous Integration
Jon Arild Tørresdal
7/13/2010 5:37 PM
Nice! If you want to use RegEx instead of SubString (not sure if that's better :-), but should be less code) you can get the file content as string (File.ReadAllText) and just use this RegEx:


Then you get 2 matches (AssemblyVersion and AssemblyFileVersion) with 4 groups in each containing major, minor, build and revision
# re: Version Assemblies with TFS 2010 Continuous Integration
9/15/2010 4:22 AM

Where does the solutionVersionFile value come from?

To get this working I have had to set it to:

SourcesDirectory + "\Source\TestSolution\SolutionAssemblyVersionInfo.cs"

This is solution sepecific, is there a way to generate the "\Source\TestSolution" part?

# re: Version Assemblies with TFS 2010 Continuous Integration
9/15/2010 9:34 AM
@Squat - the solutionVersionFile comes in as a parameter that is passed to the workflow activity (you'll see it near the bottom of the third screen shot). That's passed through from the TFS build definition so it's completely configurable.
# re: Version Assemblies with TFS 2010 Continuous Integration
Andrew Benson
11/2/2010 2:43 PM
I'm with Squat in that I'm not sure how to target the solution file within the build process. I've specified it in the Build Definition as simply "SolutionAssemblyVersionInfo.cs" but I suspect that it needs either a full path, or a relative path (but based on what???) Am I missing something here?
# re: Version Assemblies with TFS 2010 Continuous Integration
11/4/2010 7:18 AM
@Andrew - As I mentioned to Squat, you can it is passed in as a parameter (which you can see in the 3rd screen shot). *And* (more specifically) the value passed in should be:

SourcesDirectory + "\SolutionAssemblyVersionInfo.cs"

That is probably the pertinent information I neglected to mention. That "SourcesDirectory" is a variable that is already available inside the xaml for you.
# re: Version Assemblies with TFS 2010 Continuous Integration
11/12/2010 5:54 AM
Hi Steve,

Very helpful article - thanks. I'm having difficulty with the two input parameters:

solutionVersionFile - I can't use the SourcesDirectory variable in my Arguments tab. I get "'SourcesDirectory' is not declared. It may be inaccessible due to its priotection level." I can, however, see SourcesDirectory in the list of Variables.

tfsBuildNumber - What do I set this to?

Thanks for any help you can provide.
# re: Version Assemblies with TFS 2010 Continuous Integration
11/14/2010 10:34 AM
@David - tfsBuildNumber should be passed in as: BuildDetail.BuildNumber

solutionVersionFile should be passed in as: SourcesDirectory + "\SolutionAssemblyVersionInfo.cs"
# re: Version Assemblies with TFS 2010 Continuous Integration
1/18/2011 9:17 AM
I would like to ask for a link to download the xaml file as my visual studio functions weird and I cannot drop any activities into an xaml file, thus I can't create the VersionAssemblies sequence you have cretaed in this example. I will include my email in case you would like to just email it.

# re: Version Assemblies with TFS 2010 Continuous Integration
1/21/2011 6:22 PM
When i integrate with the new template and when it trued to access the solutionasseblyversioninfo.cs fileits giving me the below error...Any one has encountered this issues
The process cannot access the file ... because it is being used by another process.
# re: Version Assemblies with TFS 2010 Continuous Integration
1/27/2011 8:39 PM
@George - I don't have a download link. I'm not sure what you mean when you said that your visual studio functions weird. Can you get *no* workflows whatsoever to function in visual studio? If so, you've got a larger problem with your install.
# re: Version Assemblies with TFS 2010 Continuous Integration
1/27/2011 8:41 PM
@jj - I have not encountered that but I would check to make sure that that file is not open by Visual Studio or any other process. Also you can use Process Monitor to see what might have open handles to that file.
# re: Version Assemblies with TFS 2010 Continuous Integration
6/27/2011 5:44 AM
Where do you deploy this custom activity? Soes it go in the GAC on TFS server???
# re: Version Assemblies with TFS 2010 Continuous Integration
6/27/2011 9:58 AM
@Shaleen - You need to follow the link I referenced at the beginning of my blog post for Jim Lamb's post on building a custom workflow activity:
# re: Version Assemblies with TFS 2010 Continuous Integration
Everett Comstock
8/12/2011 3:48 PM
Hey Steve,
Thanks for the great post. I'm running into a problem with the solutionVersionFile argument in your example. I am able to define it just fine, and I pass in the value "SourcesDirectory + "\SolutionAssemblyVersionInfo.cs"" like you stated in the post comments. The issue seems to be that the "SourcesDirectory" is not evaluating. When I print the output of the solutionVersionFile argument to the Build Summary, the value shows "SourcesDirectory + "SoultionAssemblyVersionInfo.cs"" instead of the path to the sources directory.
Is it possible that I am missing some references, or maybe need to define the a variable to store the SourcesDirectory value?

# re: Version Assemblies with TFS 2010 Continuous Integration
8/14/2011 2:29 PM
@Everett - OK, this is an aspect I didn't explain well seeing as you are not the only one with this question. Here is how it all works:
1. In the "Initialize Variables" sequence (which can be found in the "Run on Agent" sequence), you'll see a GetBuildDirectory activity. This built-in TFS activity assigns the build directory to a variable called "BuildDirectory".
2. A couple of activities later, you'll see an Assign activity called "Initialize Sources Directory". This is assigning to a variable called "SourcesDirectory".
3. Now when the "AssignVersionInfo" activity is called, the value passed to the "solutionVersionFile" parameter should be: SourcesDirectory + "\SolutionAssemblyInfo.cs"

That should be everything you need.
# re: Version Assemblies with TFS 2010 Continuous Integration
9/15/2011 8:06 AM
Hi Steve :)

First of all thank you for the work you put into the making of this post.
It was a great help for us.

Here are the points the newbie xaml guy (myself) was missing from the information provided (and puts here to help the next unfortunate newbie ;) :

* The xaml file have to be created AFTER the cs file is made, else the code activity does not appear in the toolbox.
* Image # 3 (with the arguments) is opened by pressing the little button in the bottom of the xaml editor called "arguments". (cutoff in your image)
* The AssemblyVersionInfo code activity must be clicked on and the three boxes filled in with "newAssemblyFileVersion", "solutionVersionFile" and "tfsBuildNumber"
* The condition must be "System.IO.File.Exists(solutionVersionFile)" as the namespace is not included
* Dragging the Activity into the build xaml is complicated. I had to create a "shadow" project (that couldnt compile) in the folder that had the ProcessTemplate.xaml and include that into this shadow project. Also referenced the activity assembly from this project. Then when I opened it in visual studio I could finally drag it into the xaml editor.
* I had some trouble understanding that I need to tie up xaml variables to input and outputs of the code activity.
# re: Version Assemblies with TFS 2010 Continuous Integration
Ian M
9/20/2011 3:41 AM
I'm having a complete nightmare with this!! Firstly the custom activity would not drag and drop onto the template, so i added the template to the project, which allowed me to drag and drop the custom activity on it, however it now says "'BuildDetail' is not declared. It may be inaccessible due to its protection level." and the same for SourcesDirectory!!! HEEEEELLLLPPP...... please.....
# re: Version Assemblies with TFS 2010 Continuous Integration
9/27/2011 6:23 AM
After some trouble I was able to make this work, here are all the steps I needed and I gathered from all the comments here:

1. Make what casper Said: On 3rd Image you need to click on "Set AssemblyVersionInfo" box and fill the variable with "newAssemblyFileVersion", "solutionVersionFile" and "tfsBuildNumber" respectively

2. On your CustomTemplate.XAML, you also need to create the newAssemblyFileVersion Output variable, just like you did on the Activity XAML.

3. On your CustomTemplate.XAML you will need to select your activity "VersionAssemblies" and go to the properties and fill its Variables with "newAssemblyFileVersion" (the variable that you've declared in point 2), SourcesDirectory + <path to you SolutionAssemblyVersionInfo.cs> you need to put all the path from the begining of your TFS filesystem until the place where its your file. You can use "LogWarning" activities to help you trace why the file its not being found.
And then set "tfsBuildNumber" to BuildDetail.BuildNumber

After all this steps, everything should work as expected :)

If something is missing, try to use LogWarning messages to write to Build log or inside Activity.cs you could use this technique:

Hope that this helps :)

# re: Version Assemblies with TFS 2010 Continuous Integration
9/27/2011 6:49 AM
I forgot one thing, if even after you put your activity assemblies inside a TFS folder and pointed to that, it continue to giving you errors that it's not founding the custom assembly, you'll need to do something like this:

"Add the namespace xmlns:<ns>="clr-namespace:<ActivityNamespace>;assembly=<AssemblyName>" to your build process' root element"

of you CustomBuildTemplate.XAML

# re: Version Assemblies with TFS 2010 Continuous Integration
10/7/2011 12:26 AM
Hi Steve,

Great post, very helpful. The one question I have, probably because I don't understand how TFS works entirely is around the revision numbers. It doesn't reset my revision numbers when I increment the major/minor build numbers. Is there an easy way that you know of that will cause the revision numbers to reset based on the major/minor numbers being different? I noticed it natively resets with the default value using a date stamp, but I can't find much information about how the build/revision numbers are incremented in TFS.

I hope I have explained that well. Essentially if I do a two builds with I will get and but if I then check in a change to the assembly info to use my next build will be and I would like it to be

# re: Version Assemblies with TFS 2010 Continuous Integration
10/7/2011 10:04 AM
@AdamE - the way the the TFS revision numbers work is that it's just a single integer being incremented. The incrementing will reset if the "Build Number Format" is changed (on the Process tab). So if you were controller major/minor from there then it would work. However, that's not the ideal way to control Major/Minor. Given it's in the C# file, the number will continue to increment.
# re: Version Assemblies with TFS 2010 Continuous Integration
6/13/2012 5:39 PM
I'm having the same problem as some people encountered. I can't seem to drop the custom activity to my build process template. I compiled the custom activity project, checked it in to TFS under my project folder. I then opened the process template .xaml file in the designer, right-click in the Toobox and select "Choose Items..." then browse to the location of the custom activity .dll. The activity did show up in the Toolbox but couldn't be dragged onto the template. What am I missing here?
# re: Version Assemblies with TFS 2010 Continuous Integration
6/14/2012 3:11 PM
There is one problem though - the revision number will be reset every day. So in your example will become the next day when the first build happens.
# re: Version Assemblies with TFS 2010 Continuous Integration
Steve Michelotti
6/14/2012 3:22 PM
@linush - the revision number will *not* be reset the next day so long as the part that comes before it is the same (as in my example).

Regarding the problem of not being able to drop the custom activity into the build process template. You should trying bringing the build process template into your own solution and making sure it compiles first. If so, then you should be able to drag the custom activity in. You might be running into this:
# re: Version Assemblies with TFS 2010 Continuous Integration
6/14/2012 3:24 PM
@Steve - yeah, you're absolutely right. The reason I saw the revision number reset was because I still had the {yearmonthday} part in the Build Number Format under Process in the build definition. Once I took it out the revision number stopped reseting itself every day.

I've also solved the the drag-and-drop problem by following what Jakob suggested in this link:

The only other thing I had to do was to strong name the custom activity assembly and register it to the GAC on my build machine, which I think people will probably also have to do.

Thank you. This article has really helped me a lot.
# re: Version Assemblies with TFS 2010 Continuous Integration
2/28/2013 1:29 PM
Thanks Steve, I found this very helpful. I too had an issue using the SourcesDirectory variable. Instead of passing in the value in the build configuration, I did the following:

1) Created a String variable called SolutionFilePath
2) Used a ConvertWorkspaceItem activity with the following parameters:
Input: BuildSettings.ProjectsToBuild(0)
Result: SolutionFilePath
Workspace: Workspace
3) Used an Assign activity:
SolutionVersionFile = System.IO.Path.Combine(Path.GetDirectoryName(SolutionFile), "SolutionAssemblyVersionInfo.cs")

Note, there is no checking for null references and I assume the first item in ProjectsToBuild has the Solution File I need. Not sure if these conditions will be universally true, but in my case they are.

Post Comment

Title *
Name *
Comment *  

View Steve Michelotti's profile on LinkedIn

profile for Steve Michelotti at Stack Overflow, Q&A for professional and enthusiast programmers

Google My Blog

Tag Cloud