Multi targeting a single .Net project to build for different framework versions
Consider this scenario:
- I have a project that relies on ASP.Net MVC and currently this project is built against the .Net framework 4.0 and MVC 4
- I want to support MVC 5 for this project
- To support MVC 5, I’d need to upgrade the project to use .Net framework 4.5
- This would mean that developers still running .Net framework 4.0 would no longer be able to use my updated project
I also don’t want to have to create different projects and assemblies that target specific MVC versions (in this case you’d end up with assembly names like MyProject.MVC4.dll and MyProject.MVC5.dll). I have seen this done with other solutions and it works but I feel it is not necessary unless your project is using specific functionality from a particular MVC version.
In my case my project will compile perfectly against MVC 4 and 5 without any codebase changes so I’m not actually targeting against a specific MVC version ( >= 4 ). If your project uses specific functionality from the different MVC versions I think you will have to output different assemblies like MyProject.MVC5.dll. In that case this post will probably help: http://blogs.korzh.com/2013/12/nuget-package-different-mvc-versions.html
The goal
The end result is that I want a single project file that has 4 build configurations:
- Debug / Release
- These will build against .Net 4.0
- This will reference MVC 4
- Debug-Net45 / Release-Net45
- This will build against .Net 4.5
- This will reference MVC 5
Each of these build configurations exports the same assembly name: MyProject.dll
I then want a single NuGet package which will install the correct DLL based on the .Net version that the user has installed.
How it’s done
Build configuration setup
To start with you’ll have a project that targets .Net framework 4.0 and references MVC 4 and 2 standard build configurations: Debug, Release.
First we need to create 2 new build configurations: Debug-Net45 and Release-Net45 and have these configurations output the DLLs to a custom folder. To do this we launch the Configuration Manager:
Create a new build configuration:
Enter the name of the new configuration and copy the configuration from it’s associated existing one. So Debug-Net45 would copy from Debug and Release-Net45 copies from Release.
Do this for both configurations – Debug-Net45 and Release-Net45.
Project setup
The next step requires manually editing your .csproj file since Visual Studio doesn’t want to normally allow you to do this. This is how you can configure each build type to target a different framework version. You’ll first need to find these 2 entries:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
Inside each of these elements you need to explicitly tell these build configurations to use .Net framework 4.0 by adding this element:
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
Next you’ll want to copy/paste both of these property groups and:
- Change the Condition to check for the new build configurations and to target .Net framework 4.5
- Change the OutputPath to have a specific .Net 4.5 folder
- Change the TargetFrameworkVersion to be .Net 4.5
You should end up with 2 new elements that look something like:
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug-Net45|AnyCPU'"> <DebugSymbols>true</DebugSymbols> <OutputPath>bin\Debug-Net45\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <DebugType>full</DebugType> <PlatformTarget>AnyCPU</PlatformTarget> <ErrorReport>prompt</ErrorReport> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <PlatformTarget>AnyCPU</PlatformTarget> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release-Net45|AnyCPU'"> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\Release-Net45\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <PlatformTarget>AnyCPU</PlatformTarget> </PropertyGroup>
Next we need to do this for the MVC references or any framework specific references you need to target. In this example it is just the MVC references and you’ll need to wrap them with an ItemGroup element, you’ll end up with something like this to target the MVC 4 libs:
<ItemGroup Condition=" '$(Configuration)'=='Debug' Or '$(Configuration)'=='Release'"> <Reference Include="System.Web.Helpers, Version=2.0.0.0, ....."> <Private>True</Private> <HintPath> ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.Helpers.dll </HintPath> </Reference> <Reference Include="System.Web.Mvc, Version=4.0.0.0, ....."> <Private>True</Private> <HintPath> ..\packages\Microsoft.AspNet.Mvc.4.0.30506.0\lib\net40\System.Web.Mvc.dll </HintPath> </Reference> <Reference Include="System.Web.Razor, Version=2.0.0.0, ....."> <Private>True</Private> <HintPath> ..\packages\Microsoft.AspNet.Razor.2.0.30506.0\lib\net40\System.Web.Razor.dll </HintPath> </Reference> <Reference Include="System.Web.WebPages, Version=2.0.0.0, ....."> <Private>True</Private> <HintPath> ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.dll </HintPath> </Reference> <Reference Include="System.Web.WebPages.Deployment, Version=2.0.0.0, ....."> <Private>True</Private> <HintPath> ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.Deployment.dll </HintPath> </Reference> <Reference Include="System.Web.WebPages.Razor, Version=2.0.0.0, ....."> <Private>True</Private> <HintPath> ..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.Razor.dll </HintPath> </Reference> </ItemGroup>
Then we need to go ahead and copy/paste this block but target the different build configurations and then swap out these MVC 4 version references with the MVC 5 version references.
Nuget package restore
For this tutorial I’m assuming you are using Nuget to reference your MVC libs and an easy way to get Nuget to play reasonably with this setup is to enable Nuget package restore for your solution – right click on your solution and click the button:
Then in your packages.config file you can manually add the MVC 5 references:
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Microsoft.AspNet.Mvc" version="4.0.30506.0" targetFramework="net40" /> <package id="Microsoft.AspNet.Razor" version="2.0.30506.0" targetFramework="net40" /> <package id="Microsoft.AspNet.WebPages" version="2.0.30506.0" targetFramework="net40" /> <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net40" /> <package id="Microsoft.AspNet.Mvc" version="5.0.0" targetFramework="net45" /> <package id="Microsoft.AspNet.Razor" version="3.0.0" targetFramework="net45" /> <package id="Microsoft.AspNet.WebPages" version="3.0.0" targetFramework="net45" /> </packages>
Now when you build your solution under the different build configurations Nuget will automatically go and get the correct MVC versions based on the current .Net Framework version.
Thats it!
With all of this in place it should ‘just work’ :) You will notice some odd looking icons in your references list in Visual Studio but it’s nothing to worry about, Visual Studio is just confused because it isn’t familiar with how you’ve tricked it!
So now when you build using your different build configurations, it will output the correct DLLs to the correct build folders:
- Both Debug & Release will output to /bin/Debug & bin/Release with MVC 4 libraries and the MyProject.dll will be compiled against .Net Framework 4.0
- Both Debug-Net45 & Release-Net45 will be output to /bin/Debug-Net45 & /bin/Release-Net45 with MVC 5 libraries and the MyProject.dll will be compiled against .Net Framework 4.5
And here’s some proof:
If you are using some automated build processes with MSBuild you can then just target the build configuration names you’d like to export.
The Nuget setup is pretty simple since you can target your dependencies by framework:
<group targetFramework="net40"> <!--between 4 and less than version 5--> <dependency id="Microsoft.AspNet.Mvc" version="(4.0.20710,5)" /> </group> <group targetFramework="net45"> <!--between 4 and less than version 6 (who knows what'll happen then)--> <dependency id="Microsoft.AspNet.Mvc" version="(4.0.20710,6)" /> </group>
Then for each of your lib files in Nuget you ensure that you output them to the framework specific /lib folders such as:
target="lib\net40"
target="lib\net45"
This process has been implemented for the Client Dependency Framework version 1.8.0 to support MVC 4 and 5 without having to change the assembly name.