Visual Studio 2012 Multi Targeting Framework Project

One day, I got duty… from my boss… that the Application I’ve been building on should be able to run in .NET 3.5, .NET 4.0 and .NET4.5. And it should be done in single project instead of create multiple projects that maintained under Visual Studio 2008 for .NET 3.5, Visual Studio 2010 for .NET 4.0 and Visual Studio 2012 for .NET 4.5. My boss wanted me to build this project on Visual Studio 2012 only, and just once I press the build button from Visual Studio, then magically multiple .NET version of this application produced.

This is cool and challenging… Time has come to Googling… and I stopped to this StackOverflow. Thanks for everyone in there, in that discussion forum. The question and all answers all very useful. After several hours struggling with my small silly brain, then my duty is done, and my boss is happy :)

Okay, so I now want to share with you about “how it’s made” by giving short walkthrough.

The Walktrough

You will create console application using Visual Studio 2012 that when you run it will display text “Hello Universe!” and will tell the .NET version and environment from where this application was created from. This application named MultiTarget.

AppOutput

 

  1. Open VS and create new console application naming it MultiTarget.
  2. Open MultiTarget.csproj
    You can open it by using external text editor, or inside Visual Studio on Solution Explorer select MultiTarget project and unload it by right click and select Unload Project from the popup menu. The MultiTarget project will looks unavailable, please select it, right click and select Edit MultiTarget.csproj.Here’s the original content of MultiTarget.csproj should looks like:

    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="$(MSBuildExtensionsPath)$(MSBuildToolsVersion)Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)$(MSBuildToolsVersion)Microsoft.Common.props')" />
    <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{99D49942-F27D-4AA7-AA28-4AA0E907B22B}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>MultiTarget</RootNamespace>
    <AssemblyName>MultiTarget</AssemblyName>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>binDebug</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>binRelease</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    </PropertyGroup>
    <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
    </ItemGroup>
    <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="PropertiesAssemblyInfo.cs" />
    </ItemGroup>
    <ItemGroup>
    <None Include="App.config" />
    </ItemGroup>
    <Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" />
    <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
    Other similar extension points exist, see Microsoft.Common.targets.
    <Target Name="BeforeBuild">
    </Target>
    <Target Name="AfterBuild">
    </Target>
    -->
    </Project>
  3. Tweaking MultiTarget.csproj
    In the first place, let’s create new property called Framework. Put this property on the top of PropertyGroup tag. So it should be looks like this:

    <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    ......
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <Framework>$(TargetFrameworkVersion.Replace("v", "NET").Replace(".", ""))</Framework>
    <FileAlignment>512</FileAlignment>
    
    .....

    This property holds function $(TargetFrameworkVersion.Replace(“v”, “NET”).Replace(“.”, “”)). This function is to build friendly name of the current target framework. It will translate the TargetFrameworkVersion property. For example if your framework is v4.5, this will be translated as NET45 in Framework property.

  4. Modify the output path.Find the <OutputPath>binRelease</OutputPath> and <OutputPath>binDebug</OutputPath> tag, and change it to be <OutputPath>bin $(Framework)Release</OutputPath> and <OutputPath>bin $(Framework)Debug</OutputPath>.This action will causing that when you build the project, the result will located under named .NET target framework. OK, let’s try that – close the MultiTarget.csproj from editor, and reload it from Solution Explorer. Then now build the project. If you check in your project bin folder you will see there is a folder named NET45 and contains Debug folder where the result of application files were built.Output-Path-NET45

    Alright, on the next steps I’ll show you how to get NET35 and NET40 get built automatically at once.

  5. Tweaking MultiTarget.csproj again
    Please open the MultiTarget.csproj file again and start edit it.

    1. Add ChildBuild property with default value True, you may locate this property tag right under Framework property that you’ve just created.
      <PropertyGroup>
      <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
      ......
      <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
      <Framework>$(TargetFrameworkVersion.Replace("v", "NET").Replace(".", ""))</Framework>
      <ChildBuild>True</ChildBuild>
      <FileAlignment>512</FileAlignment>
      .....
    2. At the very bottom of file you might see this:
      <Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" />
      <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
      Other similar extension points exist, see Microsoft.Common.targets.
      <Target Name="BeforeBuild">
      </Target>
      <Target Name="AfterBuild">
      </Target>
      -->

      Please replace the commented tags above with this:

      <Target Name="AfterBuild"  Condition=" '$(ChildBuild)' == 'True' ">
      
         <!-- Build NET35 -->
         <MSBuild Condition=" '$(TargetFrameworkVersion)' != 'v3.5' " Projects="$(MSBuildProjectFile)" ToolsVersion="3.5" Properties="TargetFrameworkVersion=v3.5; ChildBuild=False" RunEachTargetSeparately="true" />
      
         <!-- Build NET40 -->
         <MSBuild Condition=" '$(TargetFrameworkVersion)' != 'v4.0' " Projects="$(MSBuildProjectFile)" ToolsVersion="4.0" Properties="TargetFrameworkVersion=v4.0; ChildBuild=False" RunEachTargetSeparately="true" />
      
         <!-- Build NET45 -->
         <MSBuild Condition=" '$(TargetFrameworkVersion)' != 'v4.5' " Projects="$(MSBuildProjectFile)" ToolsVersion="4.0" Properties="TargetFrameworkVersion=v4.5; ChildBuild=False" RunEachTargetSeparately="true" />
      
      </Target>

      This is where multiple version of framework will be built. We just put task for MSBuild to build this project again after one build is done, of course with different parameter based on different condition too.

      <Target Name="AfterBuild"  Condition=" '$(ChildBuild)' == 'True' ">

      It is tells that this task should be done only when ChildBuild property is set to True. See the previous step that we set the ChildBuild property is True as default. So after the default target of framework is done, then MSBuild will do another 3 tasks (Build NET35, Build NET40 and Build NET45).

      On those each task ChildBuild property is set to False so there will be no scary recursive build done there.

      And also in every task we define the TargetFrameworkVersion. This is the key to tell MSBuild to build the project in appropriate .NET Framework.

      <!-- Build NET40 -->
      <MSBuild Condition=" '$(TargetFrameworkVersion)' != 'v4.0' " Projects="$(MSBuildProjectFile)" ToolsVersion="4.0" Properties="TargetFrameworkVersion=v4.0; ChildBuild=False" RunEachTargetSeparately="true" />

      See the code above, that this task will only run when the TargetFrameworkVersion in the task is different with current TargetFrameworkVersion being build. So building multiple version will done simultaneously.

      You also have to specify the ToolsVersion property for each different target framework.

      Each task also set the property RunEachTargetSeparately to True, that it’s mean the MSBuild task invokes each target in the list passed to MSBuild one at a time, instead of at the same time.

    Now, close this file, reload it and rebuild again. Please check again on your bin folder – and you should see NET35, NET40 and NET45 folder created.

    Output-Path-All

  6. Time to prove!
    So far, we never get in touch with the code, so to accomplish our goal, let’s touch it then.  Please open program.cs file and put this code:

    using System;
    
       namespace MultiTarget {
          internal class Program {
          private static void Main(string[] args) {
             const string version =
             #if NET35
                "3.5";
             #elif NET40
                "4.0";
             #elif NET45
                "4.5";
             #endif
    
             Console.WriteLine("Hello Universe! My .NET version is {0} in enviroment {1}", version, Environment.Version);
             Console.ReadKey();
          }
       }
    }

    See there are set of unknown conditional compilation symbols (NET35, NET40 and NET45). Of course you will get error if you build this project. That was because none of those conditional compilation symbols was defined. OK, so let’s define it.

    Let’s tweak the MultiTarget.csproj again – tired unload, and reload the project back and fort, eh? I promise this is the last time :)

    Put this property group as the latest of property group in the project XML file:

    <PropertyGroup>
       <DefineConstants>$(DefineConstants);$(Framework)</DefineConstants>
    </PropertyGroup>

    Notice that never put that PropertyGroup on the top of file, otherwise it will not works because the DefineConstants can be replaced by the DefineConstants tags bellow it.

    If you read the value of the tag, it was only for adding the FrameWork property value at the last of already defined constants.

    OK, done – now as usually, close the file, reload and rebuild the project again (whew..!). Go to your bin folder and run every MultiTarget.exe inside Debug folder under each framework folder. You should see different results there.

  7. Change the default framework manually.For your convenient, you can also change your default framework target during development. As a default, now you’re developing your application in .NET 4.5, this is the default Framework of Visual Studio 2012 project.In some reason, you want to check all logic and debug your .NET 3.5 version, then it should be easily done by opening the project property, under Application tab please change Target Framework to be .NET Framework 3.5.

    Change-Manually-1

    To see that it’s been working you can check it on Build tab, the Conditional compilation symbols should be automatically changed to NET35.

    Change-Manually-2

    You can check on the code as well that variable addressed to directive NET35 is active.

    Change-Manually-3

For the solution that contains many projects you might want to improve by moving DefineConstants tag and AfterBuild target into one resuable MSBuild.targets file under solution folder and import it into every project.

OK, that’s all – hope this helps. Any thoughts or improvements are appreciated. I apologize for my English, I’m not really good on it, my English teacher was…. Hmm… you know… we were never had a good relationship :(

Thanks for reading!

 

adiono