Targeting multiple .Net framework versions in a single Visual Studio solution

From my archive - originally published on 19 May 2014

When you’re building components in .Net it’s inevitable that you’ll eventually have to target more than one version of the framework. Not every system is able to upgrade in step with new releases of the framework or even take advantage of in-place upgrades.

For example, Microsoft may be set to withdraw support for Windows 2003 but there will be systems stuck on this environment for some time to come. That means having to provide components that are compiled against .Net 4.0 as these systems will not be compatible with any later framework version.

You can always let the lowest common denominator win and compile against the lowest supported framework. The problem with this approach is that over time you will find a dwindling number of third party libraries providing updates that you can use.

It is better to take an approach based upon graceful degradation where you focus on the more advanced framework but provide a version that can be consumed by older frameworks. This is best done through a single set of source code rather than managing forks or branches.

Project files

You can run multiple project files in the same directory that are targeted for different versions of the framework. Newtonsoft’s Json.Net demonstrates how this is done as it contains numerous project versions targeting .Net versions 2.0 and onwards. Both the application and unit test projects compile for each targeted framework using the same set of files.

Note that you will need to ensure that each project files compiles output into different locations. You have to set this in two places:

  • The path for the final output is set in the “Output path” field in the “Build” tab of the project properties window. This needs to be set to a unique value for each build configuration.
  • You will also need to set the intermediate output path so parallel builds do not collide. This is set by adding a new element to the non-conditional PropertyGroup element in the project file, i.e.
<PropertyGroup>
  <BaseIntermediateOutputPath>obj\net40\</BaseIntermediateOutputPath>

Handling language differences

One disadvantage of multiple framework versions is that it becomes ever more difficult to leverage new language and framework features. You risk being bound by the lowest common denominator. For example, if you want to take advantage of the async and await keywords then you either target 4.5 or bring the Micrsofot.Bcl libraries into your project for .Net 4.0.

You can use conditional compilation statements to inject code that will only be executed for a particular framework. This approach is a little dangerous as your code will inevitably become peppered with conditional compilation logic. It’s much the same as having different source files for each framework, except the code can be difficult to read and any changes can be very difficult to keep track of.

Microsoft do not offer any built-in solutions for conditional compilation by framework version so you have to set up your own constants. These are defined in the project file for each build configuration as shown below:

<DefineConstants>TRACE;DEBUG;NET40</DefineConstants>

The constants can also be set in the “Build” tab of the project properties screen by entering them into the “Conditional compilation symbols” field. Once entered, a constant is available to a conditional compilation statement as shown below:

#if NET40
  // This code compiles
#else
  // This code does not compile
#endif

Managing and restoring NuGet packages

Dependencies are easy to manage for multiple versions so long as you are using the same packages across each build. Managing any divergence can be difficult as Nuget package restore does not support more than one view of the dependencies in a packages.config file.

Note that the package restore is not the same as the references used in each project. In most cases the references that you use in older versions will be static – i.e. they will be stuck on the last supported version. You can always reserve package restore for the current version of your code while older versions are just references in your legacy projects.

If you really want to support automatic package restore then you can create placeholder projects for each framework version that contain direct references to any NuGet packages that are used. These projects should will force Visual Studio to download any necessary packages when the project first compiles.