In this post I show how you can build .NET projects that target .NET Framework versions on Linux, without using Mono. By using the new Microsoft.NETFramework.ReferenceAssemblies NuGet packages from Microsoft you don't need to install anything more than the .NET Core SDK!

tl;dr; To build .NET Framework libraries on Linux, add the following to your project file: <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" Version="1.0.0-preview.2" />

Background: Building full-framework libraries on Linux

If you're building .NET Standard NuGet packages, and you want to provide the best experience for your users (and avoid some dependency hell) then you'll want to check out the advice on cross-platform targeting. There's a lot of DOs and DON'Ts there, but I tend to boil it down to the following: if you're targeting any version of .NET Standard, then you need the following target frameworks at a minimum:

<TargetFrameworks>netstandard2.0;net461;net472</TargetFrameworks>

If you're targeting .NET Standard 1.x too then add that in to the mix, the important point is to include the two .NET Framework targets to avoid issues with the .NET Standard 2.0 shim.

This gives a bit of an issue - the full .NET framework targets mean the library can theoretically only be built on Windows. In a previous post I showed how to work around this for Linux by installing Mono and using the assemblies it provides. In that post I showed that you could actually run a .NET Framework test suite too. This has worked very well for me so far but it has a couple of down sides

  • It requires you install Mono
  • It requires adding a somewhat hacky .props file
  • It's not officially supported, so if it doesn't work, you're on your own

The .props file sets the FrameworkPathOverride MSBuild variable to the Mono reference assemblies which is how we are able to build. But as Jon Skeet points out in this comment, Mono isn't actually required. We just need a way of easily getting the reference assemblies to compile against. This is what Microsoft have provided with the Microsoft.NETFramework.ReferenceAssemblies package.

Retrieving reference assemblies from NuGet with Microsoft.NETFramework.ReferenceAssemblies

I was completely unaware of the Microsoft.NETFramework.ReferenceAssemblies NuGet until I saw this tweet by Muhammad Rehan Saeed:

This is really good news - all you need to build libraries that target full .NET Framework on Linux is the .NET Core SDK!

Let's take an example. You can create a .NET Core class library using dotnet new classlib, which will give a .csproj file that looks something like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

</Project>

Rename the TargetFramework element to TargetFrameworks and add the extra targets:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net461;net472</TargetFrameworks>
  </PropertyGroup>

</Project>

If you try and build with dotnet build on Linux at this point, you'll see an error something like the following:

/usr/share/dotnet/sdk/2.1.700/Microsoft.Common.CurrentVersion.targets(1175,5): 
error MSB3644: The reference assemblies for framework ".NETFramework,Version=v4.6.1" 
were not found. To resolve this, install the SDK or Targeting Pack for this framework 
version or retarget your application to a version of the framework for which you have 
the SDK or Targeting Pack installed. Note that assemblies will be resolved from the 
Global Assembly Cache (GAC) and will be used in place of reference assemblies. 
Therefore your assembly may not be correctly targeted for the framework you intend.

Now for the magic. Add the Microsoft.NETFramework.ReferenceAssemblies package to your project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net461;net472</TargetFrameworks>
  </PropertyGroup>

  <!-- Add this reference-->
  <ItemGroup>
    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" Version="1.0.0-preview.2" />
  </ItemGroup>

</Project>

and suddenly the build succeeds!

Build succeeded.
    0 Warning(s)
    0 Error(s)

Using the PrivateAssets attribute prevents the Microsoft.NETFramework.ReferenceAssemblies package from "leaking" into dependent projects or published NuGet packages; it's a build-time dependency only.

Simply having to add a package to your library is a much better experience than having to explicitly install Mono. On top of that, this will be the supported approach from now on. But it gets better - in the .NET Core 3.0 SDK, the reference assembly packages will be automatically used if necessary, so in theory there's no workarounds required at all!

How it works: a meta-package, a .targets, and lots of dlls

If you're interested in how the package works, I suggest reading the relevant issue, but I'll provide a high-level outline here.

We'll start with the Microsoft.NETFramework.ReferenceAssemblies NuGet package itself. This package is a meta-package that contains no code, but has a dependency on a different NuGet package for each of the supported .NET Framework versions:

The Microsoft.NETFramework.ReferenceAssemblies nuget.org page showing its dependencies

So for example, for the .NET Framework 4.6.1 target, the meta-package depends on Microsoft.NETFramework.ReferenceAssemblies.net461. This approach ensures that you only download the reference assemblies for the framework versions you're actually targeting.

If you open up one of the Framework-specific NuGet packages you'll find two things in the build folder:

  • A .targets file
  • A .NETFramework folder full of the reference assemblies (>100MB!)

The .targets file serves a similar purpose to the .props file in my previous post - it tells MSBuild where to find the framework libraries. The example below is from the .NET 4.6.1 package:

<Project>
  <PropertyGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') And ('$(TargetFrameworkVersion)' == 'v4.6.1') ">
    <TargetFrameworkRootPath>$(MSBuildThisFileDirectory)</TargetFrameworkRootPath>

    <!-- FrameworkPathOverride is typically not set to the correct value, and the common targets include mscorlib from FrameworkPathOverride.
         So disable FrameworkPathOverride, set NoStdLib to true, and explicitly reference mscorlib here. -->
    <EnableFrameworkPathOverride>false</EnableFrameworkPathOverride>
    <NoStdLib>true</NoStdLib>
  </PropertyGroup>

  <ItemGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') And ('$(TargetFrameworkVersion)' == 'v4.6.1') ">
    <Reference Include="mscorlib" Pack="false" />
  </ItemGroup>

</Project>

This sets the TargetFrameworkRootPath parameter to the folder containing the .targets file. MSBuild traverses down the NuGet package's folder structure (.NETFramework\v4.6.1), to find the dlls, and finds the reference assemblies:

The reference assemblies downloaded in the NuGet package

I've tested out the packages using both the .NET Core 2.1 SDK and 2.2 SDK, and it's worked brilliantly both times. Give it a try!

Resources