Imagine you want to create a library that supports different versions of .NET.  Of course there are multiple ways to do this but this is the technique that I have adopted for my CodePlex projects.

My Requirements Are

Get Microsoft Silverlight
Download video

Project Files

The first obstacle is project files.  The project file has a target framework version which applies to all files in the project

This means that I will have to have a separate project file for each version of the framework.  The convention I adopted is to append the target framework name on the end of the project file name.

For example

Solution Files

Solution files are simply a way to organize project files.  Sometimes I want to open just a specific set of projects, sometimes I want to open all of them.  As with the project files I create a set of solution files with version specific names and then one solution that includes all versions.

Project Constants

Most of my projects involve Windows Workflow Foundation (WF4) which has some types that were introduced in .NET 4.0.1 and remain in .NET 4.5.  I wanted to have a set of constants that I could use for conditional compilation in my projects.  Here is the set I came up with

I could have edited my project files manually to define the constants that that matched the target framework but instead I decided to create a couple of .targets files that automatically defines the constants I need based on the target framework and modify the path properties as required.

I also wanted the output path of Activity Designer assemblies to be set to the matching Activity project.  By convention the name of the designer assembly is (ActivityProject).Design.dll.  So if I find a project name that ends with Design then I will set its output to the matching (ActivityProject) output directory.  This will allow Visual Studio to find the designer file when building the project.

Setting this up required me to create a couple of different .targets files so that the build would work from the command line with MSBuild and also from within Visual Studio. 

The first targets file MultiVersion.Before.Targets runs before the Microsoft.Common.targets file so it can alter the paths used by the build to include version specific folders.

The second targets file MultiVersion.After.Targets runs after the Microsoft.Common.targets file so it can do things like copying the version specific config files over the app.config or web.config and also take care of some quirky behavior with Visual Studio where it ignores the IntermediateOutputPath property when building for x86 or x64 targets.

To make this work you have to set a couple of custom properties  to the first property group in your project file.

 

XML
Edit|Remove
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"  <PropertyGroup    <!-- Other stuff --> 
    <!-- Custom Targets to setup the framework specific paths and build properties --> 
    <CustomBeforeMicrosoftCommonTargets>$(MSBuildProjectDirectory)\..\MultiVersion.Before.targets</CustomBeforeMicrosoftCommonTargets> 
    <CustomAfterMicrosoftCommonTargets>$(MSBuildProjectDirectory)\..\MultiVersion.After.targets</CustomAfterMicrosoftCommonTargets> 
  </PropertyGroup> 
 

 

Conditional Compilation

In your code you can now use conditional compilation by referring to the constants you want to use.  For example with Workflow sometimes I want to refer to StateMachine specific things like the StateMachineStateRecord when tracking.

 

C#
Edit|Remove
protected override void Track(TrackingRecord record, TimeSpan timeout) 
{ 
    if (record is WorkflowInstanceAbortedRecord) 
    { 
        this.OnRaiseAbortedEvent(new WorkflowInstanceAbortedEventArgs(record)); 
    } 
    else if (record is WorkflowInstanceRecord) 
    { 
        this.OnRaiseWorkflowInstanceEvent(new WorkflowInstanceEventArgs(record)); 
    } 
#if NET401_OR_GREATER 
    // StateMachineStateRecord is not in .NET 4.0.0 
    else if (record is StateMachineStateRecord) 
    { 
        this.OnRaiseStateMachineEvent(new StateMachineEventArgs(record)); 
    } 
#endif 
} 
I’ve also found that sometimes third party refactoring tools don’t handle conditional compilation as well as I’d like so one technique I’ve used is to add a partial class for version specific things.  Here you can see that the EventTracker class
that I’ve created has a separate file for the StateMachine specific things

 

Sometimes it isn’t possible to keep everything in a separate file.  In such cases you can use
compilation directives but be careful around using statements.

 

C#
Edit|Remove
namespace MultiVersionLibrary 
{ 
    using System; 
#if NET401_OR_GREATER 
    // Watch out - tools that sort using statements may move this using outside of the compile directive 
    using System.Activities.Statements.Tracking; 
#endif 
    using System.Activities.Tracking

Assembly Attributes

 

When building a group of related projects it can be useful to create a single shared file that contains the assembly attributes and include it in each project as a linked file.

Visual Studio won’t allow you to directly add a linked file to the Properties folder but you can add it to the main project as a link and then drag / drop it into the properties folder. Or if you prefer just add it directly to the project file.

 

XML
Edit|Remove
  <ItemGroup    <Compile Include="..\Globals.1.0.1.cs"      <Link>Properties\Globals.1.0.1.cs</Link> 
    </Compile> 
    <Compile Include="Program.cs" /> 
    <Compile Include="Properties\AssemblyInfo.cs" /> 
  </ItemGroup> 
In the AssemblyInfo file I’ve created a class with no namespace that the Global file refers to in order to pick up the assembly title.  Other than that all the attributes come from the Globals file.

 

With debug builds I add the target version so I can quickly see which version of the file I’m working with.

Configuration Files

The build system copies app.config files to the target directory and renames them for you. Likewise with web.config files it may do a config transform as a part of the build. In some cases you may want to have a version specific config file.

To do this you simply create app.(version).config and add it to your project alongside app.config.  At build time the appropriate app.(version).config will overwrite app.config so don’t edit app.config by accident.  Note: You must include app.config in your project for Visual Studio to copy it to the output directory.

Debugging Projects / Targets files

I spent a great deal of time trying to figure this all out.  MSBuild logs are your best weapon to
troubleshoot these things. 

To enable diagnostic logging in Visual Studio, select Tools / Options / Projects and Solutions / Build and Run.


To enable diagnostic logs with MSBuild on the command line use the following command

msbuild MultiVersionLibrary.sln /fl /v:diag

Then open msbuild.log to get a lot of great information.