Multi-targeting .NET 4.6, .NET 4.7, UAP 10.0 with a single project

After waiting for the project.json storm to lay down, it was time for me to check whether the new .NET SDK project system was worth checking out. Oh dear, it was! In this post I will try to explain our reasoning to migrate to the new project system and how we overcame some of the issues we encountered during the transition.

Requirements

One of the companies I work for use a large amount of (open source) libraries target .NET 4.5, .NET 4.6 and .NET 4.7. The reason we supported 3 different target frameworks per library is that it allowed us to switch the runtime of the (WPF) apps at any time and the libraries would be ready for it. However, a big downside of this is that loading the solution takes a long time, and everything is duplicated as you can see in the picture below:

While we are (considering to) making the transition to UWP, this would introduce a 4th platform project, meaning we would have 4 + 1 (for the shared project) = 5 projects per library.

Old solution structure

Time to make a change! After reading the very promising blog post by Oren Novotny, I decided to make the jump and try and convert all the libraries to the new SDK project to achieve the following goals:

For this blog post, I will be using a fake repository named Ghk.MultiTargeting as an example. Note that this project is useless, but contains per-platform xaml code and shared code-behind.

Project differences compared to the original template

Compared to the original multi-targeting example, we made the following changes.

Project defines

To make sure I could still use the defines we invested in (to maximize code-sharing, we use a lot of #if in code shared between WPF and UWP). This project creates the following defines:

Include solution assembly info

The project structure adds this line to the project to include the SolutionAssemblyInfo and GlobalSuppressions files which are in the root of the solution.

<Compile Include="..\*.cs" />

Allow shared code-behind (e.g. MyView.xaml.cs) with a *per platform* view (e.g. MyView.xaml)

To allow sharing of code-behind of a view (for example, for custom controls), we need to to a bit of hacking. We create the view inside each platform specific view directory:

After creating the views, we will need to customize the code-behind to add a partial method:

public partial class MyView
{
     public MyView()
     {
         InitializeComponent();

         Construct();
     }

     partial void Construct();
}

Next, we create a partial code-behind class inside the regular views folder that contains the shared code:

This view should contain the following code and you can share your code-behind but have per-platform xaml files:

public partial class MyView
{
     partial void Construct()
     {
         // TODO: Add shared constructor info here
     }
}

As an example, I added a dependency property in the shared code-behind that shows how to deal with the different platforms in shared code using defines.

Final project structure

Below is the final project structure that can serve as a reference.

Note that it is recommended to check out the latest version in the example repository.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net46;net47;uap10.0</TargetFrameworks>
    <AssemblyName>Ghk.MultiTargeting</AssemblyName>
    <RootNamespace>Ghk.MultiTargeting</RootNamespace>
    <DefaultLanguage>en-US</DefaultLanguage>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors />
    <AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
    <PackageId>Ghk.MultiTargeting</PackageId>
    <PackageVersion>1.0.0-alpha0001</PackageVersion>
    <Authors>GeertvanHorrik</Authors>
    <Description>Multi targeting example library.</Description>
    <PackageProjectUrl>https://github.com/GeertvanHorrik/Ghk.MultiTargeting</PackageProjectUrl>
    <PackageLicenseUrl>https://github.com/GeertvanHorrik/Ghk.MultiTargeting/blob/develop/LICENSE</PackageLicenseUrl>
    <RepositoryUrl>https://github.com/GeertvanHorrik/Ghk.MultiTargeting</RepositoryUrl>
    <PackageTags>automatic;support</PackageTags>
    <PackageOutputPath>$(ProjectDir)..\..\output\$(ConfigurationName)\</PackageOutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MSBuild.Sdk.Extras" Version="1.2.2" PrivateAssets="all" />
    <PackageReference Include="Catel.Core" Version="5.3.0" />
    <PackageReference Include="Catel.MVVM" Version="5.3.0" />
    <PackageReference Include="Catel.Fody" Version="2.20.0" PrivateAssets="all" />
    <PackageReference Include="Fody" Version="2.4.6" PrivateAssets="all" />
    <PackageReference Include="ModuleInit.Fody" Version="1.7.1" PrivateAssets="all" />
    <PackageReference Include="Obsolete.Fody" Version="4.3.7" PrivateAssets="all" />
    <PackageReference Include="WpfAnalyzers" Version="2.1.2" PrivateAssets="all" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Update="NETStandard.Library" Version="2.0.1" />
  </ItemGroup>
   
  <Import Project="$(MSBuildSDKExtrasTargets)" Condition="Exists('$(MSBuildSDKExtrasTargets)')" />

  <ItemGroup>
    <Compile Include="..\*.cs" />
    <Compile Remove="Platforms\**\*.cs;Resources\**\*.cs" />
    <Page Remove="Platforms\**\*.xaml" />
    <!-- This is here so that the conditionally included files below are all visible in VS -->
    <None Include="Platforms\**\*.*;Resources\**\*.*" />
  </ItemGroup>

  <ItemGroup>
    <Compile Update="Properties\Resources.Designer.cs">
      <DesignTime>True</DesignTime>
      <AutoGen>True</AutoGen>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <EmbeddedResource Update="Properties\Resources.de.resx">
      <Generator></Generator>
    </EmbeddedResource>
    <EmbeddedResource Update="Properties\Resources.es.resx">
      <Generator></Generator>
    </EmbeddedResource>
    <EmbeddedResource Update="Properties\Resources.fr.resx">
      <Generator></Generator>
    </EmbeddedResource>
    <EmbeddedResource Update="Properties\Resources.nl.resx">
      <Generator></Generator>
    </EmbeddedResource>
    <EmbeddedResource Update="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <EmbeddedResource Update="Properties\Resources.ru.resx">
      <Generator></Generator>
    </EmbeddedResource>
  </ItemGroup>

  <!-- .NET Standard 2.0 -->
  <PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <DefineConstants>NETSTANDARD;NETSTANDARD2_0;NS;NS20</DefineConstants>
  </PropertyGroup>
  
  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <Compile Include="Platforms\netstandard2.0\**\*.cs" />
    <Page Include="Platforms\netstandard2.0\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
    <Compile Update="Platforms\netstandard2.0\**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />
  </ItemGroup>

  <!-- .NET 4.6 -->
  <PropertyGroup Condition=" '$(TargetFramework)' == 'net46' ">
    <DefineConstants>NET;NET46</DefineConstants>
  </PropertyGroup>
  
  <ItemGroup Condition=" '$(TargetFramework)' == 'net46' ">
    <Compile Include="Platforms\net\**\*.cs" />
    <Page Include="Platforms\net\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
    <Compile Update="Platforms\net\**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />
  </ItemGroup>

  <!-- .NET 4.7 -->
  <PropertyGroup Condition=" '$(TargetFramework)' == 'net47' ">
    <DefineConstants>NET;NET47</DefineConstants>
  </PropertyGroup>
  
  <ItemGroup Condition=" '$(TargetFramework)' == 'net47' ">
    <Compile Include="Platforms\net\**\*.cs" />
    <Page Include="Platforms\net\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
    <Compile Update="Platforms\net\**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />
  </ItemGroup>
  
  <!-- UAP 10.0 -->
  <PropertyGroup Condition=" '$(TargetFramework)' == 'uap10.0' ">
    <DefineConstants>UAP;NETFX_CORE</DefineConstants>
  </PropertyGroup>
  
  <ItemGroup Condition=" '$(TargetFramework)' == 'uap10.0' ">
    <Compile Include="Platforms\uap10.0\**\*.cs" />
    <Page Include="Platforms\uap10.0\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
    <Compile Update="Platforms\uap10.0\**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />
  </ItemGroup>
</Project>

Bonus: Cake deployments

Since we also migrated to using Cake as build script tooling, this is a good opportunity to take a look at Cake as well. I added the full build scripts we use for our components in the example repository. Note that you only have to change the variables in build.cake in the root and all should be updated.

References