TFSTips TFS TeamBuild doesn't copy references (Assemblies)

As a TFS administrator, I often have to solve the same issue again and again: new developers complain that referenced assemblies are not dropped by TeamBuild although locally, Visual Studio copies those references into the bin folder... The trick is to set the property "Copy Local" = "False" on the references to be copied, Save the project, reset the property "Copy Local" = "True" and Save again the project.

Click to Read More

We have experienced this issue at least with VS 2010 - TFS 2010 and VS 2013 - TFS 2013. I have to say I don't remember about VS 2008 - TFS 2008  and VS 2005 - TFS 2005.

Project's "Copy Local" property is the one that indicates if a file reference must be copied or not in the output folder. The value of that property is stored in a tag <Private> in the project file (.csproj, .vbproj, ...). Ex.: <Private>true</Private>

The problem is that VS does not add this tag for references whose 'Copy Local' property is 'true', 'true' being the default value for file references added on assemblies not in the GAC. It only adds this tag if one changes the property value to 'false'. Later, if one sets the value back to 'true', the tag is kept but its value is changed.

This is a problem because MSBuild, run by TeamBuild to compile the projects, assume that the value of a "Copy Local" property is 'False' if the tag <Private> is not found in the project file.

So, the trick is to force VS to add the tag for all references added with 'Copy Local'='true'. This can be done as explained above:  set "Copy Local" = "False" on the required references, Save the project, reset "Copy Local" = "True" and Save again the project.

ProgrammingTFS TFS 2013 plugins cannot load dependencies located in the "Plugins" folder

I have a plugin for TFS 2010 that reads a config file containing custom section handlers. When I deployed my plugin on TFS 2013, it thrown the following error when trying to instantiate those section handlers :

A first chance exception of type 'System.Configuration.ConfigurationErrorsException' occurred in System.Configuration.dll

Additional information: An error occurred creating the configuration section handler for xxxxx: Could not load file or assembly 'xxxxx' or one of its dependencies. The system cannot find the file specified.

The problem was that there was no adequate probing path defined for .NET 4.5 in the TFS 2013 Services' web config.

Click to Read More

TFS 2013 Services are loading plugins from C:\Program Files\Microsoft Team Foundation Server 12.0\Application Tier\Web Services\bin\Plugins.

But once those plugins loaded, they can only load their own dependencies from the GAC or from the TFS 2013 Services' bin folder (C:\Program Files\Microsoft Team Foundation Server 12.0\Application Tier\Web Services\bin). I.e.: the sub-folder 'Plugins' of that bin folder is not probed...

In my case, when my plugin read its custom section, it needs to load the assembly containing the related custom section handlers (actually, the plugin and the custom handlers are in the very same assembly...). It works fine if I deploy the assembly in TFS 2013 Services' bin folder (or in the GAC I presume) but I cannot bring myself  to simply do that. Especially taking into account that it was working fine within TFS 2010.

Having a look at TFS 2013 Services' web config, in order to specify my own probing path, I noticed that there was already one defined, but ONLY for .Net 2.0. I.e.: here is the <runtime> section of C:\Program Files\Microsoft Team Foundation Server 12.0\Application Tier\Web Services\web.config

<!-- Plugin directory path -->
<runtime>
<assemblyBinding appliesTo="v2.0.50727" xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="bin\Plugins;bin\Plugins\zh-chs;bin\Plugins\zh-cht;bin\Plugins\de;bin\Plugins\es;bin\Plugins\fr;bin\Plugins\it;bin\Plugins\ja;bin\Plugins\ko"/>
<dependentAssembly>
<assemblyIdentity name="System.Web.Extensions" publicKeyToken="31bf3856ad364e35"/>
<bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Extensions.Design" publicKeyToken="31bf3856ad364e35"/>
<bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>

As my plugin targets .Net 4.5, I simply defined the probing path for all target runtime.

<!-- Plugin directory path -->
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="bin\Plugins;bin\Plugins\zh-chs;bin\Plugins\zh-cht;bin\Plugins\de;bin\Plugins\es;bin\Plugins\fr;bin\Plugins\it;bin\Plugins\ja;bin\Plugins\ko"/>
</assemblyBinding>

<assemblyBinding appliesTo="v2.0.50727" xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.Extensions" publicKeyToken="31bf3856ad364e35"/>
<bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Extensions.Design" publicKeyToken="31bf3856ad364e35"/>
<bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>

And that solved my issue !

FYI (and google indexing): my plugin is a Server Side Event Handler for TFS...

TFS Web Site projects, TFS and branches can mess up your workspace...

Yet another issue experienced when working on multiple branches in TFS: working with branched Visual Studio Web Site Projects (not to be confused with Visual Studio Web Application Projects) could indeed result in the creation of messy mappings in your workspace. This is due to the way Visual Studio manages the Virtual Directories behind Web Site Projects.

Click to Read More

My colleagues reported indeed some troubles when working with Visual Studio 2010 on Visual Studio Solutions containing Web Sites (not Web Applications) branched, e.g., from an existing TFS folder $/TeamProject/Main/Website onto a new branch $/TeamProject/Dev/WebSite (They still have indeed a few such Web Site Projects. Those cannot be upgraded into Web Applications as they use a framework that relies on various typical features of web sites, not supported by web applications. In addition, note that on the development workstations, the web sites are hosted in IIS due to some prerequisites of that framework, a.o. some limitation on the host header name and port).

Assume now that $/TeamProject is mapped withing a local workspace on C:\TFS\TeamProject, that there is no other mapping in this workspace and that one checks-in a new Web Site created in C:\TFS\TeamProject\Dev\WebSite. By default, the resulting server items will be located in  $/TeamProject/Dev/WebSite. And as you should know, a virtual directory named "WebSite" will have been created in IIS by Visual Studio and mapped on C:\TFS\TeamProject\Dev\WebSite behind the scene - mapping which is saved in the Visual Studio suo file).

Next, assume that one does a reverse integration of the Dev branch on the Main branch, that one checks-in the pending changes and that one opens next the Visual Studio Solution located in C:/TFS/TeamProject/Main (opening de facto also the web site)

Using Web Application Projects instead of a Web Site Projects, Visual Studio would alert the user that a Virtual Directory already exists with the same name but with another physical path (C:/TFS/TeamProject/Dev/WebSite in this case). And the user would be prompted to possibly redefine this Virtual Directory with the new location (i.e.: with C:/TFS/TeamProject/Main/WebSite). If the user refuses, the Web Application won't be loaded in Visual Studio. If he agrees, the Virtual Directory is remapped on the new location.

But with Web Site Projects, it's slightly different.... Visual Studio prompts the user to reuse the Virtual Directory. If the user refuses, a new Virtual Directory with the same name but suffixed with "_1" is created and mapped on the new physical path (opening again the web site from yet another physical location would results in suffixes "_2", "_3", etc...). This new name is unfortunately an issue for my colleagues, if they want to run the web site, again due to some constraints of the framework in use.

Instead, if the user accepts to reuse the Virtual Directory (expecting a behavior similar to the one experienced with Web Application Projects), Visual Studio will not redefine the physical location of the Virtual Directory; it will create new mappings in the workspace to get the sources from TFS in the physical location currently defined for the existing Virtual Directory. I.e.: the definition of the workspace will be like:

  • $/TeamProject                          ==>  C:/TFS/TeamProject
  • $/TeamProject/Main/WebSite ==>  C:/TFS/TeamProject/Dev/WebSite
And the Virtual Directory "WebSite" will be kept mapped on the physical location C:/TFS/TeamProject/Dev/WebSite.

Guess what if the user reopens later the solution in C:/TFS/TeamProject/Dev to implement new features on the web site in the Dev branch ? He will actually check-in his changes under the branch $/TeamProject/Main/

Upgrade to Web Applications is not an option at all - it was already investigated years ago and the cost was to high. But I could possibly investigate the use of the Visual Studio Development Web Server instead of IIS, although it is a prerequisite for the framework used in those web sites projects.

As a temporary solution, I suggest all users to delete any existing Virtual Directory before opening a Solution containing Web Sites Projects. If they have already reused an existing Virtual Directory, I suggest them to clean the messy mappings in their workspace and delete next the existing Virtual Directory. If they have a Solution working with a Virtual Directory suffixed with _1, I suggest them to delete their .suo file (otherwise Visual Studio will always recreate a Virtual Directory with that name when opening the Web Site Projects) and delete that Virtual Directory.

Notice: in some case (if not deleting/recreating correctly the Virtual Directory and the suo), you could have Visual Studio Solution files opened from various branches referencing the same Virtual Directory, obviously mapped on only one physical path, but without extra mappings in the workspace... And Visual Studio won't prompt you to use the existing Virtual directory when opening a solution. In such a case, delete the suo file of that solution.

TFS MSSCCI, TFS and branches can mess up your workspace...

At work, some colleagues are developing a standalone application hosted within the Visual Studio Isolated Shell. This application makes use of the DSM extension for Visual Studio to generate source code based on diagrams, source code which should be stored within TFS. Unfortunately, Team Explorer does not integrate with the Visual Studio Isolated Shell :(

So, they use MSSCCI as a solution to access TFS. But they recently noticed that this solution was messing up their workspace when working in multiple branches.

Click to Read More

TFS can be accessed from the Visual Studio Isolated Shell using the MSSCCI provider for Team Foundation (<= here the link for the 32bits/VS 2010 version).

My colleagues reported however some troubles when working on Visual Studio Solutions branched, e.g., from an existing TFS folder $/TeamProject/Main onto a new branch $/TeamProject/Dev.

Assume that $/TeamProject is mapped in a local workspace on C:\TFS\TeamProject, that there is no other mapping in this workspace and that one checks-in a new Visual Studio Solution created in C:\TFS\TeamProject\Dev (with one sub-folder per included Visual Studio Project). By default, the resulting server items will be located in  $/TeamProject/Dev with all projects' sub-folders created "recursively" bellow.

Next, assume that one does a reverse integration of the Dev branch on the Main branch, that one checks-in the pending changes and that one opens next the Visual Studio Solution located in C:/TFS/TeamProject/Main.

It appears that, behind the scene, Visual Studio with MSSCCI will create new workspace mappings between $/TeamProject/Main/xxx and C:/TFS/TeamProject/Dev/xxx. Any "Get Latest" made on the Main folder in TFS will update the local Dev subfolders! And when one will later open the Visual Studio Solution in this local Dev folder, expecting to work on the Dev branch, one will actually be working on the Main branch ;)

The reason is that the Visual Studio Isolated Shell with MSSCCI is using absolute paths in the Visual Studio Solution Files as references to included Visual Studio Project files. Visual Studio with Team Explorer is using relative paths instead. And when one opens a Visual Studio Solution Files containing absolute paths, mappings are created in the workspace, between the "server" items and those local "absolute paths". I.e.: the definition of the  workspace will be like:

  • $/TeamProject                          ==>  C:/TFS/TeamProject
  • $/TeamProject/Main/Project1   ==>  C:/TFS/TeamProject/Dev/Project1
  • $/TeamProject/Main/Project2   ==>  C:/TFS/TeamProject/Dev/Project2

If you ignore that absolute paths are used in your Visual Studio Solution file and if you don't pay attention to the server path of your pending changes, you will really be confused when your colleagues will complain that you didn't check-in your new features in the right branch or when a Gated Build will start on the Main branch when you check-in your changes :D

TFSTips Assembly Version, File Version and Product Version: The Butterfly Effect

Assembly Version, File Version and Product Version are "related" if you don't specify them explicitly... and you could be surprised by one of the side effect: loosing you application data or user settings when incrementing the Assembly File Version.

Click to Read More

How are those versions specified:

By default, any new Visual Studio Project includes an AssemblyInfo file defining default values (major, minor, build and revision) for the Assembly Version and Assembly File Version:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
  • A difference in the build number represents a recompilation of the same source. This is usually appropriate because of processor, platform, or compiler changes.
  • Assemblies with the same name, major, and minor version numbers but different revision numbers are intended to be fully interchangeable. This is usually appropriate for security fix, etc ...
  • “[assembly: AssemblyVersion("65534.65534.65534.65534")]” is maximum.

MSBuild can auto-increment the build and revision numbers at each build if we provide a (*) in place of those figures. Ex.

[assembly: AssemblyVersion("1.0.*")]

or

[assembly: AssemblyVersion("1.0.0.*")]

In such cases, the build number is generated based on the current day and the revision number is generated based on the number of seconds since midnight. Replacing the revision number only with a (*) would be irrelevant. Indeed, consecutive builds would most probably have lower revisions.

There is no default value set for the Product Version when creating a new Visual Studio Project. But a value can be set manually in the AssemblyInfo file using:

[assembly: AssemblyInformationalVersion("1.0.0.0")]

Usually, the Product Version is a value like major.minor.build, but it can actually be any string, like "1.0 RC", etc... Don't use (*) in this string as it wouldn't be replaced by an auto-incremented figure. Instead, it would crash your application in some cases (See further for the explanation). Notice also that before VS 2010, using anything else that major.minor.build.rev was resulting in  false warning during compilation.

What do those versions mean:

Assembly Version : This is the version number used by framework during build and at runtime to locate, link and load the assemblies. When you add reference to any assembly in your project, it is this version number which gets embedded. At runtime, CLR looks for assembly with this version number to load. But remember this version is used along with name, public key token and culture information only if the assemblies are strong-named signed. If assemblies are not strong-named signed, only file names are used for loading.
 
Assembly File Version : This is the version number given to file as in file system. It is displayed by Windows Explorer. Its never used by .NET framework or runtime for referencing. But it can be used by OS tools.

Product Version (a.k.a Assembly Informational Version): Defines an additional version information for an assembly manifest. This is the version you would use when talking to customers or for display on your website..

What if a version is not specified:

  • If the Assembly version is not explicitly specified, it takes the value of 0.0.0.0.
  • If the Assembly File version is not explicitly specified, it takes the value of the Assembly version.
  • If the Product version is not explicitly specified, it takes the value of the Assembly File version.
How could issues occurred due to this relation:

By your fault :)

At work, we recently decided to manage the major and minor numbers of the Assembly Versions at build time, within our TFS Build Process Template. Assembly Versions' build and release numbers are kept equal to 0 while major and minor are enforced with values provided through Build Definitions' parameters. At the same time,  we auto-increment the build and release numbers of the Assembly File Versions while keeping their major and minor numbers aligned with those of the Assembly Versions. We don't touch the Product Version. This was designed as recommended in KB 556041.

As mentioned, major and minor numbers are specified through custom parameters of our Build Definitions. The build number is computed to reflect directly the current day using a format like "YMMdd" with Y = year modulo x to be <= 6 (None of our product has a longer life). The release number is set with the auto-incremented value provided out-of-the box by Team Build.

Previously, most of the binaries were compiled locally, on development workstations, and there was no version management at all (There was simply no need for any such versioning): Assembly Version and Assembly File Version were always kept unchanged.

The change introduced with our Build Process Template had an unexpected consequence: As there never was any Product Version specified explicitly in the AssemblyInfo, binaries' Product Versions were always equal to the Assembly File Version... kept therefore unchanged through all builds. However, since we started to auto-increment the Assembly File Version, the Product Version started to increment too...

And ? And some users started to complain that their custom settings were lost after the installation of each new version...

We quickly noticed that, as usually recommended, developers were making use the two following properties to store  respectively application's data and users' data:

Application.CommonAppDataPath
Application.LocalUserAppDataPath

And after some investigations on MSDN, it appeared that the path returned by those properties depend among other on the Product Version :(

Depending on the OS, CommonAppDataPath is usually like:

  • %SystemDrive%\Documents and Settings\All Users\Application Data\<CompanyName>\<ProductName>\<ProductVersion> or
  • %SystemDrive%\ProgramData\<CompanyName>\<ProductName>\<ProductVersion>

And LocalUserAppDataPath is like:

  • "%SystemDrive%\Documents and Settings\<UserId>\Local Settings\Application Data\<CompanyName>\<ProductName>\<ProductVersion>" or
  • "%SystemDrive%\Users\<UserId>\AppData\Local\<CompanyName>\<ProductName>\<ProductVersion>".

So, indeed, each new release of our product having now a new Product Version, application's data and users' data of the previous versions are not used anymore...

Any (*) used in the Product Version (e.g.: 1.0.*) wouldn't be replaced by auto-incremented build/release numbers (as mentioned previously), the "AppDataPath"  above would contain an illegal character; reason why the application would crash when accessing this path.

This also impacted applications accessing settings in the registry with methods like:

Application.UserAppDataRegistry.SetValue()
Application.CommonAppDataRegistry.SetValue()

Those methods access respectively data under 

  • HKCU\Software\<CompanyName>\<ProductName>\<ProductVersion>\
  • HKLM\Software\<CompanyName>\<ProductName>\<ProductVersion>\
Read also this paper on User Settings Management.
See also one of my sources here about this topic.

TFSTips Drop Builds' primary output in a "Latest" folder

As briefly mentioned in another post, we have a specific "Reference Assemblies" folder on the Build Machines containing the latest version of each assembly issued from a successful Build. Assemblies in that folder, know as the "Latest" folder, can be used for "Continuous Integration" Builds.  This post is about the pragmatic solution implemented to drop only the primary output of the Builds in that folder.

Click to Read More

The output of a Build contains not only the assemblies and satellite assemblies issued from the compilation of the Visual Studio Projects (I.e.: assemblies part of the "primary output" as named in the Microsoft Setup Projects). It contains also a copy of all the referenced assemblies (file references) with the property "CopyLocal"="True".

It's important for our purpose to only drop the primary output into the "Latest" folder, otherwise we could override the latest version of some referenced assemblies with a  copy of the specific version found for the Build (e.g.: when targeting the Integration Environment, we use the promoted version on, the assemblies which are possibly not the latest).

We may not set "CopyLocal"="False" on all the "file references" because MSTest needs a copy of those in the bin folder to be able to run the unit Tests (That would not be the case if we could find for MSTest an equivalent of the "ReferencePath" parameter of MSBuild).

We don't have access to methods (or well described "algorithms") to retrieve the exact list of assemblies part of the "primary output". Such methods are only implemented in the Microsoft Setup Projects (Projects not supported, by the way, by MSBuild).

We don't want all our developers to add MSBuild Scripts in their Visual Studio Projects to drop the "Targets" in the "Latest" folder.

Ex. copy "$(TargetDir)$(TargetName).???" "C:\RefAssemblies\Latest"

This is not only too error prone (like everything you request to a human developer), but it's also not robust enough. It's in our opinion impossible to maintain this with enough guarantee taking into account that developers can for example add support for new languages at any time. For each Project, in addition to the assembly $(TargetDir)$(TargetName).dll, we also have to drop the satellite assemblies (e.g.: <culture>\<assemblyname>.resources.dll"), etc...

The most generic solution we found to identify the assemblies part of the primary output, although quick and dirty, consists in parsing the FileListAbsolute.txt file generated by MSBuild itself and available in the "obj" folder. We "rely" so on direct output of Microsoft (the content of that file) and avoid to implement (and maintain) our own generic (and most probably complex) algorithm ("complex" because I have no idea how to detect satellite assemblies in a Visual Studio Project).

Notice: We may only drop the primary output of Builds in the "Latest" folder for "Continuous Integration" Builds or Builds targeting the Integration Environment (both using the latest version of the sources). Builds targeting Qualification, Pre-Production or Production use always a specific version of the sources instead of the latest version. Their purpose is indeed to fix a bug in the assemblies deployed in the targeted environment and build with older sources.

Click to Read More

using System;
using System.IO;
using System.Linq;
using System.Activities;
using System.Reflection;
using System.Diagnostics;
using System.ComponentModel;
using System.Collections.Generic;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.Build.Evaluation;

namespace AG.SCRM.TeamBuild.Activity
{
    /// <summary>
    /// Collect Build's Primary Output Items
    /// </summary>
    [BuildActivity(HostEnvironmentOption.All)]
    public sealed class GetPrimayOutput : CodeActivity
    {
        /// <summary>
        /// List of Build's Primary Output Items
        /// </summary>
        [RequiredArgument]
        public InOutArgument<List<FileInfo>> PrimaryOutputItems { get; set; }

        /// <summary>
        /// Build's Configuration Parameter
        /// </summary>
        [RequiredArgument]
        public InArgument<string> Configuration { get; set; }

        /// <summary>
        /// Build's Platform Parameter
        /// </summary>
        [RequiredArgument]
        public InArgument<string> Platform { get; set; }

        /// <summary>
        /// Local path of Build's project file
        /// </summary>
        [RequiredArgument]
        public InArgument<string> LocalProject { get; set; }


        [RequiredArgument]
        public InArgument<string> OutDir { get; set; }

        /// <summary>
        /// Collect Build's Primary Output Items
        /// </summary>
        /// <param name="context"></param>
        protected override void Execute(CodeActivityContext context)
        {
            var fileListAbsolute = PrimaryOutputItems.Get(context);
            var localProject = LocalProject.Get(context);
            var configuration = Configuration.Get(context);
            var platform = Platform.Get(context);

            // Default Configuration is "Debug"
            if (string.IsNullOrEmpty(configuration)) configuration = "Debug";

            // Initialize the list of Build's Primary Output Items if required.
            // Otherwise, add Build's Primary Output Items to the provided list
            // to possibly support a loop on all Visual Studio Solution files in 
            // Build's 
            if (fileListAbsolute == null) fileListAbsolute = new List<FileInfo>();

            if (Path.GetExtension(localProject).Equals(".sln", StringComparison.InvariantCultureIgnoreCase))
            {
                // Parse the Visual Studio Solution file
                SolutionParser sln = new SolutionParser(localProject);
                string root = Path.GetDirectoryName(localProject);
                foreach (SolutionProject project in sln.MSBuildProjects)
                {
                    localProject = Path.Combine(root, project.RelativePath);
                    CollectOutputItems(context, fileListAbsolute, configuration, platform, localProject);
                }
            }
            else
            {
                // Validate that the file is a Visual Studio Project
                var projectCollection = new ProjectCollection();
                try
                {
                    Project project = projectCollection.LoadProject(localProject);
                    string projectDirectoryPath = Path.GetDirectoryName(localProject);
                    CollectOutputItems(context, fileListAbsolute, configuration, platform, localProject);
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("Project file '{0}' is not a valid Visual Studio project.", localProject), ex);
                }
                projectCollection.UnloadAllProjects();

            }

            context.SetValue(PrimaryOutputItems, fileListAbsolute);
        }

        private static void CollectOutputItems(CodeActivityContext context, List<FileInfo> items, string configuration, string platform, string project)
        {
            string projectFileName = Path.GetFileName(project);
            string projectName = Path.GetFileNameWithoutExtension(project);
            string projectPath = Path.GetDirectoryName(project);
            string fileListAbsolute = GetFileListAbsolute(configuration, platform, projectFileName, projectPath);

            if (!string.IsNullOrEmpty(fileListAbsolute))
            {
                System.IO.StreamReader file = null;
                string line;


                string parent = Path.GetDirectoryName(fileListAbsolute);

                // Read the file and parse it line by line.
                using (file = new System.IO.StreamReader(fileListAbsolute))
                {
                    while ((line = file.ReadLine()) != null)
                    {
                        // Ignore obj folder's local items.
                        if (!line.StartsWith(parent, StringComparison.OrdinalIgnoreCase))
                        {
                            FileInfo item = new FileInfo(line);
                            if (item.Exists)
                            {
                                // We are actually only interested in .dll and .exe + their .pdb, .xml and .config
                                if (item.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) || item.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))
                                {
                                    //MessageHelper.DisplayInformation(context, string.Format("Build output of {0} contains: {1}.", projectName, item.Name));
                                    items.Add(item);

                                    var config = new FileInfo(Path.Combine(item.DirectoryName, item.Name + ".config"));
                                    if (config.Exists)
                                    {
                                        //MessageHelper.DisplayInformation(context, string.Format("Build output of {0} contains: {1}.", projectName, config.Name));
                                        items.Add(item);
                                    }

                                    var pdb = new FileInfo(Path.Combine(item.DirectoryName, Path.GetFileNameWithoutExtension(item.FullName) + ".pdb"));
                                    if (pdb.Exists)
                                    {
                                        items.Add(item);
                                    }

                                    var xml = new FileInfo(Path.Combine(item.DirectoryName, Path.GetFileNameWithoutExtension(item.FullName) + ".xml"));
                                    if (xml.Exists)
                                    {
                                        items.Add(item);
                                    }
                                }
                            }
                            else
                            {
                                //MessageHelper.DisplayWarning(context, string.Format("Primary Output items not found for project {0}: {1}.", projectName, line));
                            }
                        }
                    }
                }
            }
            else
            {
                //MessageHelper.DisplayWarning(context, string.Format("SCRM: No FileListAbsolute found for project '{0}'", projectName));
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="configuration">Build's Configuration: Debug or Release</param>
        /// <param name="platform">Build's Target Platform: Any CPU or x86</param>
        /// <param name="projectFileName">Visual Studio Project filename</param>
        /// <param name="projectPath">Visual StudioProject path</param>
        /// <returns>The path of the FileListAbsolute.txt file.</returns>
        /// <remarks>This file should be located under /obj/{platform}/{configuration/}. 
        /// But we also check the parent folders if the file is not found where expected.
        /// We did indeed experienced problem to locate this file for Builds with "Mixed Plateform" Target.</remarks>
        private static string GetFileListAbsolute(string configuration, string platform, string projectFileName, string projectPath)
        {
            var fileListAbsoluteName = projectFileName + ".FileListAbsolute.txt";
            var fileListAbsolutePath = Path.Combine(projectPath, "obj", platform, configuration, fileListAbsoluteName);
            if (!File.Exists(fileListAbsolutePath))
            {
                fileListAbsolutePath = Path.Combine(projectPath, "obj", configuration, fileListAbsoluteName);
                if (!File.Exists(fileListAbsolutePath))
                {
                    fileListAbsolutePath = Path.Combine(projectPath, "obj", fileListAbsoluteName);
                    if (!File.Exists(fileListAbsolutePath))
                    {
                        fileListAbsolutePath = null;
                    }
                }
            }

            return fileListAbsolutePath;
        }
    }

    /// <summary>
    /// This Visual Studio Solution file Parser can be used to retrieve lists of projects in a Solution.
    /// The first list contains all projects that can be built with MSBuild.
    /// The second list contains all the other projects.
    /// </summary>
    /// <remarks>
    /// Based on http://stackoverflow.com/questions/707107/library-for-parsing-visual-studio-solution-files.
    /// It's a wrapper on Microsoft Build's internal class "SolutionParser"
    /// </remarks>
    public class SolutionParser
    {
        static readonly Type s_SolutionParser;
        static readonly PropertyInfo s_SolutionParser_solutionReader;
        static readonly MethodInfo s_SolutionParser_parseSolution;
        static readonly PropertyInfo s_SolutionParser_projects;

        public List<SolutionProject> MSBuildProjects { get; private set; }
        public List<SolutionProject> OtherProjects { get; private set; }

        static SolutionParser()
        {
            s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false);
            if (s_SolutionParser != null)
            {
                s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance);
                s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance);
                s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance);
            }
        }

        public SolutionParser(string solutionFileName)
        {
            if (s_SolutionParser == null)
            {
                throw new InvalidOperationException("Cannot find type 'Microsoft.Build.Construction.SolutionParser' are you missing a assembly reference to 'Microsoft.Build.dll'?");
            }
            var solutionParser = s_SolutionParser.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(null);
            using (var streamReader = new StreamReader(solutionFileName))
            {
                s_SolutionParser_solutionReader.SetValue(solutionParser, streamReader, null);
                s_SolutionParser_parseSolution.Invoke(solutionParser, null);
            }
            MSBuildProjects = new List<SolutionProject>();
            OtherProjects = new List<SolutionProject>();
            var array = (Array)s_SolutionParser_projects.GetValue(solutionParser, null);
            for (int i = 0; i < array.Length; i++)
            {
                SolutionProject project = new SolutionProject(array.GetValue(i));

                if (project.ProjectType == "KnownToBeMSBuildFormat")
                    MSBuildProjects.Add(project);
                else
                    OtherProjects.Add(project);
            }
        }
    }

    /// <summary>
    /// This class represent a Visual Studio Project member of a Solution.
    /// </summary>
    /// <remarks>
    /// It's a wrapper on Microsoft Build's internal class "ProjectInSolution"
    /// </remarks>
    [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}, {ProjectType}")]
    public class SolutionProject
    {
        static readonly Type s_ProjectInSolution;
        static readonly PropertyInfo s_ProjectInSolution_ProjectName;
        static readonly PropertyInfo s_ProjectInSolution_RelativePath;
        static readonly PropertyInfo s_ProjectInSolution_ProjectGuid;
        static readonly PropertyInfo s_ProjectInSolution_ProjectType;

        static SolutionProject()
        {
            s_ProjectInSolution = Type.GetType("Microsoft.Build.Construction.ProjectInSolution, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false);
            if (s_ProjectInSolution != null)
            {
                s_ProjectInSolution_ProjectName = s_ProjectInSolution.GetProperty("ProjectName", BindingFlags.NonPublic | BindingFlags.Instance);
                s_ProjectInSolution_RelativePath = s_ProjectInSolution.GetProperty("RelativePath", BindingFlags.NonPublic | BindingFlags.Instance);
                s_ProjectInSolution_ProjectGuid = s_ProjectInSolution.GetProperty("ProjectGuid", BindingFlags.NonPublic | BindingFlags.Instance);
                s_ProjectInSolution_ProjectType = s_ProjectInSolution.GetProperty("ProjectType", BindingFlags.NonPublic | BindingFlags.Instance);
            }
        }

        public string ProjectName { get; private set; }
        public string RelativePath { get; private set; }
        public string ProjectGuid { get; private set; }
        public string ProjectType { get; private set; }

        public SolutionProject(object solutionProject)
        {
            this.ProjectName = s_ProjectInSolution_ProjectName.GetValue(solutionProject, null) as string;
            this.RelativePath = s_ProjectInSolution_RelativePath.GetValue(solutionProject, null) as string;
            this.ProjectGuid = s_ProjectInSolution_ProjectGuid.GetValue(solutionProject, null) as string;
            this.ProjectType = s_ProjectInSolution_ProjectType.GetValue(solutionProject, null).ToString();
        }
    }

    /// <summary>
    /// List of known project type Guids from http://www.mztools.com/articles/2008/mz2008017.aspx
    /// + BizTalk: http://winterdom.com/2008/12/biztalkserver2009msbuildtasks
    /// + Workflow 4.0
    /// </summary>
    public enum ProjectType
    {
        [Description("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}")]
        Windows_CSharp,
        [Description("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}")]
        Windows_VBNET,
        [Description("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}")]
        Windows_VisualCpp,
        [Description("{349C5851-65DF-11DA-9384-00065B846F21}")]
        Web_Application,
        [Description("{E24C65DC-7377-472B-9ABA-BC803B73C61A}")]
        Web_Site,
        [Description("{F135691A-BF7E-435D-8960-F99683D2D49C}")]
        Distributed_System,
        [Description("{3D9AD99F-2412-4246-B90B-4EAA41C64699}")]
        Windows_Communication_Foundation_WCF,
        [Description("{60DC8134-EBA5-43B8-BCC9-BB4BC16C2548}")]
        Windows_Presentation_Foundation_WPF,
        [Description("{C252FEB5-A946-4202-B1D4-9916A0590387}")]
        Visual_Database_Tools,
        [Description("{A9ACE9BB-CECE-4E62-9AA4-C7E7C5BD2124}")]
        Database,
        [Description("{4F174C21-8C12-11D0-8340-0000F80270F8}")]
        Database_other_project_types,
        [Description("{3AC096D0-A1C2-E12C-1390-A8335801FDAB}")]
        Test,
        [Description("{20D4826A-C6FA-45DB-90F4-C717570B9F32}")]
        Legacy_2003_Smart_Device_CSharp,
        [Description("{CB4CE8C6-1BDB-4DC7-A4D3-65A1999772F8}")]
        Legacy_2003_Smart_Device_VBNET,
        [Description("{4D628B5B-2FBC-4AA6-8C16-197242AEB884}")]
        Smart_Device_CSharp,
        [Description("{68B1623D-7FB9-47D8-8664-7ECEA3297D4F}")]
        Smart_Device_VBNET,
        [Description("{14822709-B5A1-4724-98CA-57A101D1B079}")]
        Workflow_30_CSharp,
        [Description("{D59BE175-2ED0-4C54-BE3D-CDAA9F3214C8}")]
        Workflow_30_VBNET,
        [Description("{06A35CCD-C46D-44D5-987B-CF40FF872267}")]
        Deployment_Merge_Module,
        [Description("{3EA9E505-35AC-4774-B492-AD1749C4943A}")]
        Deployment_Cab,
        [Description("{978C614F-708E-4E1A-B201-565925725DBA}")]
        Deployment_Setup,
        [Description("{AB322303-2255-48EF-A496-5904EB18DA55}")]
        Deployment_Smart_Device_Cab,
        [Description("{A860303F-1F3F-4691-B57E-529FC101A107}")]
        Visual_Studio_Tools_for_Applications_VSTA,
        [Description("{BAA0C2D2-18E2-41B9-852F-F413020CAA33}")]
        Visual_Studio_Tools_for_Office_VSTO,
        [Description("{F8810EC1-6754-47FC-A15F-DFABD2E3FA90}")]
        SharePoint_Workflow,
        [Description("{6D335F3A-9D43-41b4-9D22-F6F17C4BE596}")]
        XNA_Windows,
        [Description("{2DF5C3F4-5A5F-47a9-8E94-23B4456F55E2}")]
        XNA_XBox,
        [Description("{D399B71A-8929-442a-A9AC-8BEC78BB2433}")]
        XNA_Zune,
        [Description("{EC05E597-79D4-47f3-ADA0-324C4F7C7484}")]
        SharePoint_VBNET,
        [Description("{593B0543-81F6-4436-BA1E-4747859CAAE2}")]
        SharePoint_CSharp,
        [Description("{A1591282-1198-4647-A2B1-27E5FF5F6F3B}")]
        Silverlight,
        [Description("EF7E3281-CD33-11D4-8326-00C04FA0CE8D")]
        BizTalk,
        [Description("32f31d43-81cc-4c15-9de6-3fc5453562b6")]
        Workflow_40
    };

    /// <summary>
    /// Helper Class to manage Visual Studio Project's types
    /// </summary>
    public static class ProjectTypeExtensions
    {
        public static Guid ToGuid(this ProjectType val)
        {
            DescriptionAttribute[] attributes = (DescriptionAttribute[])val.GetType().GetField(val.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
            return attributes.Length > 0 ? Guid.Parse(attributes[0].Description) : Guid.Empty;
        }

        public static ProjectType Parse(string val)
        {
            return Parse(Guid.Parse(val));
        }

        public static ProjectType Parse(Guid val)
        {
            ProjectType? type = null;
            FieldInfo[] fis = typeof(ProjectType).GetFields();
            foreach (FieldInfo fi in fis)
            {
                DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
                if (attributes.Length > 0)
                {
                    if (Guid.Parse(attributes[0].Description) == val)
                    {
                        if (Enum.IsDefined(typeof(ProjectType), fi.Name))
                            type = (ProjectType)Enum.Parse(typeof(ProjectType), fi.Name);
                        break;
                    }
                }
            }
            if (type.HasValue)
                return type.Value;
            else
                throw new FormatException(string.Format("'{0}' is not a valid Project Type's Guid", val.ToString()));
        }

        public static List<ProjectType> GetMSBuildProjectTypes(string localProject)
        {
            var projectCollection = new ProjectCollection();
            Project project;
            try
            {
                project = projectCollection.LoadProject(localProject);
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Project Type cannot be determined as '{0}' is not a valid VS project.", localProject), ex);
            }

            var projectTypes = GetMSBuildProjectTypes(project);

            projectCollection.UnloadAllProjects();

            return projectTypes;
        }

        public static List<ProjectType> GetMSBuildProjectTypes(Project project)
        {
            try
            {
                var projectTypeGuids = (from property in project.Properties
                                        where property.Name == "ProjectTypeGuids"
                                        select property.EvaluatedValue).FirstOrDefault();

                List<ProjectType> projectTypes;
                if (string.IsNullOrEmpty(projectTypeGuids))
                    projectTypes = new List<ProjectType>();
                else
                    projectTypes = (from guid in projectTypeGuids.Split(';') select Parse(guid)).ToList();

                return projectTypes;
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Unable to determine the project type of '{0}' due to: {1}", Path.GetFileNameWithoutExtension(project.FullPath), ex.Message));
            }
        }
    }
}

TFS Customize TFS 201x - my sources of information

Team Foundation or Team Build can be customized a lot with e.g. our own Build Process Templates, Build Activities or Plug'ins (like TFS Server-Side Event Handlers since TFS 2010). The biggest issue is usually to locate the required Microsoft assemblies on our PC and find the relevant documentation about the "TFS API's".

Click to Read More

On my own, I had among other to:

  1. Extend TFS with a plugin to check Policies server-side:.
    • This one enforces our branching strategy,
    • Check quota usage and maximum file size,
    • Restrict the file types that can be stored in TFS (no ISO, VHD, MKV - you can't imagine what end-users can try to store in source control),
    • Validate project folders hierarchy according to our Design patterns, etc...

    Implementing Policies server-side is minimizing the maintenance efforts (nothing to distribute on all the development workstations), fixes are effective immediately after deployment, Policies are enforced for all clients and nobody can bypass them, ...

  2. Create customize Build Process Templates to
    • Build Microsoft Setup Projects and Biztalk Projects,
    • Support automatic build deployment,
    • Support online Server maintenance and graceful TFS reboot (waiting on current build to complete before updating or rebooting the server),
    • Validate Build Definitions against our Policies (Build Timeouts, Drop folders location, etc...).

    For any task to be run on Build Servers, we use Custom Build Activities and Custom Build Process Templates, so those tasks can be easily integrated within the Build Schedule, ...

  3. And finally, create Visual Studio Plugins to automate some TFS operations, e.g.:
    • Right click a Build to retrieve all its binaries and their sources in order to pass them to our in-house SCRM tool (a.o.: for backward compatibility between TFS and our Release Distribution processes),
    • Right click a Build to automatically create an Maintenance Branch from its sources in order to implement some fixes, ...

I used to be quite comfortable with the customization of Team Build 2005 and 2008, using custom complex MSBuild Scripts. But I didn't find it easy to start the customization of Build Process Templates using the online MSDN documentation available for Team Build 2010 (i.e.: actually nothing relevant). I did also initially find difficult to implement the same features server side and client side as equivalent TFS API's are implemented in distinct Microsoft Assemblies and have distinct signatures...

Fortunately, there are always pioneers who are digging new stuff and publishing their findings on the web. So nowadays we can find a lot of information although the MSDN online documentation is still really poor. Here are my three main sources of information:

And so far, it seems to me that the changes between TFS 2010 and TFS 2012 are much smaller than between TFS 2008 and TFS 2010. Knowledge only needs to be extended a bit, and not completely renewed. And again, we can count on great blogs to help.

TFSTips Target multiple environments with only one TFS Build Server

I had customize our Build Process Template to support multiple target environments on a single Build Machine. I.e.: to resolve the "file references" at Build time depending on the environment targeted for the deployment.

Click to Read More

At work, we have mainly four distinct environments: Integration, Qualification, Pre-Production and Production. Each application (and its referenced assemblies) are promoted from one environment to the next one via tasks (Release Distribution jobs) scheduled according to a predefined monthly Release Plan...

TFS Build Machines not only compile the applications to be deployed via the standard Release Distribution, but also the urgent fixes to be deployed directly in Pre-Production without passing through Integration and Qualification environments. It also happens, although really quite seldom, that a Build skips the Integration and goes directly to Qualification.

It's also to be noticed that none of our applications (assemblies) are strong named and deployed in the GAC, neither on development workstations nor on servers. Instead, all referenced assemblies are always copied within the application's bin folders, also on the servers.

Therefore, basically, we would need Build Machines dedicated for each target environment with the adequate assemblies made available on them, for reference resolution purpose. That would be a pity to have such Build Machines (and the related setup, maintenance, backup costs, ...) for at most one build per month (Fix in Production and Qualification are fortunately not common).

To avoid that,  I did customize our Build Process Template to take a Reference Path as input and to pass it to MSBuild. Actually, when editing a Build Definition, the Builder can select the target Environment, and the Build will simply receive a path to the location containing the related assemblies.

Ex.: MSBuild mySolution.sln /p:ReferencePath="c:\RefAssemblies\Qualification\"

How can I be sure that this Reference Path won't interfere with any Hint Paths defined in the Visual Studio projects?

First, note that the location provided to MSBuild via the "ReferencePath" parameter will be probed to resolve all "file references" before any other location on the standard probing path. But we also pay attention to not make the assemblies available on the Hint Path on the Build Machines:

  • On Development Workstations, all our assemblies are made available in a single "Reference Assemblies" folder. Developers add references on assemblies in there for development and local testing purpose. They can also start a task at will to update this "Reference Assemblies" folder with the latest version of the assemblies (the versions used in the Integration Environment) or with the version currently deployed in the Qualification or Production Environments (e.g.: for bug fixing purpose).
  • On the Build Machines, there is one "Reference Assemblies" folder per environment (i.e.: updated by the Release Distribution with the assemblies currently in use in that environment). None of those folders is located at the same path as the "Reference Assemblies" folder of the Development Workstations. As a result, MSBuild cannot use the Hint Paths found in the Visual Studio projects to locate the referenced assemblies. Instead, it uses the path of the "Reference Assemblies" folder passed to it via its parameter /p:ReferencePath.

In addition to the "Reference Assemblies" folders per environment, we also have one  extra folder containing the output of each latest successful Build. This one is used for "Continuous Integration". No need to update the references in the Visual Studio Projects, MSBuild always find in that folder the latest version of each assembly recently built on any Build Machine (if requested to do so via the Build Definition). This is by the way the default "target environment" for all "Rolling Builds" defined on "Development Branches", so any breaking features in a new version of a referenced assembly is immediately detected. Builds "candidate" to be promoted use by default the Reference Path with the assemblies from the Integration environment.

TFSTips Connect to TFS from VS with multiple accounts

Assuming that you are using Windows Integration Authentication to access TFS, any instances of Visual Studio run during a Windows session will always use the current user to access TFS... Fortunately, there is a simple trick to connect on TFS with multiple accounts without starting new Windows sessions with other users.

Click to Read More

If like me you are a TFS administrator, you could indeed need to switch quite often from one user account to another. e.g.: to work in TFS with another profile (Contributor or Builder) and validate various configuration settings (security, etc...).

In such a case, the easiest is to right-click on the Visual Studio Shortcut (e.g.: in the Start Menu >  All Programs > etc...) and select "Run As...". Then enter the windows account to be used to connect to TFS.

Notice: Each time you will do that for the first time with a new account, Visual Studio could start much slower than usually as it has to create a new profile...

Notice: The account used to "Run As..." Visual Studio won't become the new default account. I.e.: it won't be reused the next time you run Visual Studio!

You can also edit the properties of the shortcut to Visual Studio (or create a copy of that shortcut). Then, click on the "Advanced" button in the "Shortcut" tab and tick the option box "Run with different credentials". Doing that, you will always be prompted to enter an account when running Visual Studio.

To permanently change the default account used by Visual Studio to connect on TFS, I read that we have to change the credentials used for the that TFS server in the "Credentials Manager" (Control Panel > User Accounts > Credential Manager). Unfortunately  I may not do that here at the office :/

To check with which user you are connected to TFS, check-out any file and have a look on the "User" column in the "Source Control Explorer".

Notice: You may run two instances of Visual Studio with distinct accounts simultaneously. There won't be any "conflict".