When designing an API or libraries, we aim to have maximum coverage of available .NET frameworks so that we can have maximum number of clients adopt our APIs. The key challenge in such scenarios is to have a clean code and an efficient way to manage multiple versions of code, Nuget packages and builds.
This article will outline a quick and easy way to manage single code base and target multiple .NET framework versions. I’ve used the same concept in KonfDB
Step 1 – Visual Studio Project Configuration
First, we need to use Visual Studio to create multiple build definitions. I would prefer 2 definitions per .NET configuration like
- .NET 4.0 — DebugNET40, ReleaseNET40
- .NET 4.5 — DebugNET45 and ReleaseNET45
When adding these configurations, clone them from Debug and Release and make sure you have selected ‘Create New Project Configurations’
This will modify your solution (.sln) file and Project (.csproj) files.
If certain projects do not support both versions, you can uncheck them before clicking on Close button. This is usually done, when your solution has 2 parts – API and Server and you want the API to be multi-framework target and Server code to run on a particular version of .NET
Step 2 – Framework Targeting in Projects
There are 2 types of changes required in the Project (.csproj) files to manage multiple .NET versions
Every project has default configuration. This is usually the lowest or base configuration. This is defined by xml property like
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
Change this to:
<Configuration Condition=" '$(Configuration)' == '' ">DebugNET40</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
Make sure that all the projects in solution have same default Configuration and TargetFrameworkVersion
When we added multiple configurations to our solution, there is one PropertyGroup per configuration added to our Project (.csproj) files. This appears something like,
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DebugNET40|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup>
We need to add/modify 3 lines in each of these PropertyGroup tags to change OutputPath, TargetFrameworkVersion and DefineConstants
For .NET 4.0:
<OutputPath>bin\$(Configuration)\$(TargetFrameworkVersion)\</OutputPath> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> <DefineConstants>DEBUG;TRACE;NET40</DefineConstants>
For .NET 4.5:
<OutputPath>bin\$(Configuration)\$(TargetFrameworkVersion)\</OutputPath> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <DefineConstants>DEBUG;TRACE;NET45</DefineConstants>
We will use these settings later in the article.
Step 3 – References Targeting in Projects
Our dependent libraries may have different versions for different versions of .NET. A classic example is Newtonsoft JSON libraries which are different for .NET 4.0 and .NET 4.5. So we may require framework dependent references – be it Standard References or Nuget References.
When we are using standard references, we can organize our libraries in framework specific folders and alter the project configuration to look like,
<Reference Include="Some.Assembly"> <HintPath>..\Libraries\$(TargetFrameworkVersion)\Some.Assembly.dll</HintPath> </Reference>
To reference Nuget packages, we can add conditions to the references as shown below
<ItemGroup> <Reference Include="Newtonsoft.Json, Version=126.96.36.199, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL" Condition="'$(TargetFrameworkVersion)' == 'v4.5'"> <HintPath>..\..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll</HintPath> </Reference> <Reference Include="Newtonsoft.Json, Version=188.8.131.52, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL" Condition="'$(TargetFrameworkVersion)' == 'v4.0'"> <HintPath>..\..\..\packages\Newtonsoft.Json.6.0.8\lib\net40\Newtonsoft.Json.dll</HintPath> </Reference> </ItemGroup>
When we now do a batch build in Visual Studio, the solution should compile without errors.
Step 4 – Managing Clean Code with multiple frameworks
There are 2 ways to manage our code with different versions of .NET.
Bridging the gap of .NET 4.5.x in .NET 4.0
Let’s assume we are creating an archival process where we want to zip the log files and delete the log files after zipping them. If we build this functionality with .NET 4.5 framework, we can use the ZipArchive class (in System.IO.Compression) in .NET 4.5 but there is no such class in .NET 4.0. In such cases, we should go for interface driven programming and define 2 implementations – one for .NET 4.0 and one for .NET 4.5.
These 2 implementations cannot co-exist in the solution as they may give compilation issues. To avoid these we need to edit the Project (.csproj) file to
<Compile Include="LogFileMaintenance40.cs" Condition=" '$(TargetFrameworkVersion)' == 'v4.0' " /> <Compile Include="LogFileMaintenance45.cs" Condition=" '$(TargetFrameworkVersion)' == 'v4.5' " />
Both these files can have the same class names as at a given time, only one of them will compile
The unclean way
The unclean way is where we use the DefineConstants to differentiate between the framework versions. Earlier in the project configuration, we changed the DefineConstants to have NET40 and NET45. We can use these DefineConstants as pre-processor directives to include framework specific code like,
#if NET40 … #endif #if NET45 … #endif
This methodology should be adopted only if there is minor change in the functionalities as it is very difficult to debug this code.
Step 5 – Build without Visual Studio
While Visual Studio allows us to trigger builds for any configuration by manually selecting the configuration from the dropdown, we can also create a batch file to allow us build our solution with different .NET frameworks. This batch file can be used with any Build System like TFS, Jenkins, TeamCity, etc.
REM Build Solution SET CONFIGURATION=%1 set PATH_SOURCE_SLN="%cd%\OurSolution.sln" if [%1]== ( SET CONFIGURATION=DebugNET40 ) MSBuild %PATH_SOURCE_SLN% /p:Configuration=%CONFIGURATION%
This 5 step process allows us to develop our solution targeting multiple .NET frameworks and allows us to narrow down the implementation to a particular .NET framework during the build.