I've been hitting Docker hard (as regulars will notice from the topic of recent posts!), and thanks to .NET Core, it's all been pretty smooth sailing. However, I had a requirement for building a library that multi-tartgets both full .NET framework and .NET Standard, to try to avoid some of the dependency hell you can get into.

Building full .NET framework apps requires that you have .NET Framework installed on your machine (or at least the reference assemblies), which is fine when I'm building locally, as I'm working on Windows. However, I wanted to build my apps in Docker on the build server, which is running on Linux.

I'd played around before with using Mono as a target, but I'd never got very far. However, I recently stumbled across this open issue which contains a number of workarounds. I gave it a try, and evntually got it working!

In this post I'll describe the steps to get an ASP.NET Core library that targets both .NET Framework and .NET Standard, building, and running tests, on Linux as well as Windows.

tl;dr; Add a .props file to your project and reference it in each project that builds on full framework. You may also need to add explicit references to some Facade assemblies like System.Runtime, System.IO, and System.Threading.Tasks.

Using Mono for running .NET Core tests on Linux

The first point worth making is that I want to be able to run on Linux under the full .NET Framework, not just build. That's an important distinction, as it means I can run unit tests across all target frameworks on both Windows and Linux.

As discussed by John Skeet in the aforementioned issue, if you just want to build on Linux and target .NET Framework, then you shouldn't need to install Mono at all - reference assemblies should be sufficient. However, .NET Core tests are executables, which means you need to actually run them. Which brings me back to Mono.

As I described in a previous post, I typically already have Mono installed in my Linux Docker images, as I'm using the full-framework version of Cake (instead of .NET Core-based Cake.CoreClr). My initial reasons for that are less relevant with the recent Cake releases, but as I already have a working build process, I'm not inclined to switch just yet. Especially if I need to use Mono for running tests anyway!

Adding FrameworkPathOverrides for Linux

Unfortunately, installing Mono is only the first hurdle you'll face if you try and build your multi-targeted .NET Core apps on Linux. If you just try running the build without changing your project, you'll get an error something like the following:

error MSB3644: The reference assemblies for framework ".NETFramework,Version=v4.5.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.

When MSBuild (which the dotnet CLI uses under-the-hood) compiles an application, it needs to use "reference assemblies" so it knows which APIs are actually available for you to call. When you build on Windows, MSBuild knows the standard locations where these libraries can be found, but for building on Mono, it needs help.

That's where the following .props file comes in. This file (courtesy of this comment on GitHub), when referenced by a project, looks in the common install locations for Mono and sets the FrameworkPathOverride property as appropriate. MSBuild uses this property to locate the Framework libraries required to build your app.

<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
  <PropertyGroup>
    <!-- When compiling .NET SDK 2.0 projects targeting .NET 4.x on Mono using 'dotnet build' you -->
    <!-- have to teach MSBuild where the Mono copy of the reference asssemblies is -->
    <TargetIsMono Condition="$(TargetFramework.StartsWith('net4')) and '$(OS)' == 'Unix'">true</TargetIsMono>

    <!-- Look in the standard install locations -->
    <BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND '$(TargetIsMono)' == 'true' AND EXISTS('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono</BaseFrameworkPathOverrideForMono>
    <BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND '$(TargetIsMono)' == 'true' AND EXISTS('/usr/lib/mono')">/usr/lib/mono</BaseFrameworkPathOverrideForMono>
    <BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND '$(TargetIsMono)' == 'true' AND EXISTS('/usr/local/lib/mono')">/usr/local/lib/mono</BaseFrameworkPathOverrideForMono>

    <!-- If we found Mono reference assemblies, then use them -->
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net45'">$(BaseFrameworkPathOverrideForMono)/4.5-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net451'">$(BaseFrameworkPathOverrideForMono)/4.5.1-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net452'">$(BaseFrameworkPathOverrideForMono)/4.5.2-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net46'">$(BaseFrameworkPathOverrideForMono)/4.6-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net461'">$(BaseFrameworkPathOverrideForMono)/4.6.1-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net462'">$(BaseFrameworkPathOverrideForMono)/4.6.2-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net47'">$(BaseFrameworkPathOverrideForMono)/4.7-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net471'">$(BaseFrameworkPathOverrideForMono)/4.7.1-api</FrameworkPathOverride>
    <EnableFrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != ''">true</EnableFrameworkPathOverride>

    <!-- Add the Facades directory.  Not sure how else to do this. Necessary at least for .NET 4.5 -->
    <AssemblySearchPaths Condition="'$(BaseFrameworkPathOverrideForMono)' != ''">$(FrameworkPathOverride)/Facades;$(AssemblySearchPaths)</AssemblySearchPaths>
  </PropertyGroup>
</Project>  

You could copy this into each .csproj file, but a better approach is to put it into a file in your root directory, netfx.props for example, and import it into each project file. For example:

<Project Sdk="Microsoft.NET.Sdk">  
  <Import Project="..\netfx.props" />

  <PropertyGroup>
    <TargetFrameworks>net452;netsandard2.0</TargetFrameworks>
  </PropertyGroup>
</Project>  

Note, I tried to use Directory.Build.props to automatically import the file into every project, but I couldn't get it to work. I'm guessing the properties are imported at the wrong time, so I think you'll have to stick to the manual approach.

With the path to the framework libraries overwritten, you're one step closer to running full framework on Linux, but you're not quite there yet.

Adding references to facade libraries

If you try the above solutions in your own projects, you'll likely see a different set of errors, complaining about missing basic types like Attribute, Task, or Stream:

CS0012: The type 'Attribute' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.0.0.0

To fix these errors, you need to add references to the indicated assemblies to your projects. You can add these libraries using a conditional, so they're only referenced when building full .NET Framework apps, but not .NET Standard or .NET Core apps:

<Project Sdk="Microsoft.NET.Sdk">  
  <Import Project="..\..\netfx.props" />

  <PropertyGroup>
    <TargetFrameworks>net452;netstandard1.5</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'net452' ">
    <Reference Include="System" />
    <Reference Include="System.IO" />
    <Reference Include="System.Runtime" />
    <Reference Include="System.Threading.Tasks" />
  </ItemGroup>

</Project>  

We're getting closer, the app builds now, but if you're running your tests with xUnit (as I was) then you'll likely see exceptions when running your tests with dotnet test.

Fixing errors in xUnit running on Mono

After adding the required facade assembly references to my test projects, I was seeing the following error in the test phase of my app build, a NullReferenceException in System.Runtime.Remoting:

Catastrophic failure: System.NullReferenceException: Object reference not set to an instance of an object

Server stack trace:  
  at System.Runtime.Remoting.ClientIdentity.get_ClientProxy () [0x00000] in <71d8ad678db34313b7f718a414dfcb25>:0 
  at System.Runtime.Remoting.RemotingServices.GetOrCreateClientIdentity (System.Runtime.Remoting.ObjRef objRef, System.Type proxyType, System.Object& clientProxy) [0x00068] in <71d8ad678db34313b7f718a414dfcb25>:0 
  at System.Runtime.Remoting.RemotingServices.GetRemoteObject (System.Runtime.Remoting.ObjRef objRef, System.Type proxyType) [0x00000] in <71d8ad678db34313b7f718a414dfcb25>:0 

Apparently this is due to some long-standing bugs in Mono related to app domains. The simplest solution was to just disable app domains for my tests.

To disable app domains, add an xunit.runner.json file to your test project, containing the following content. If you already have a xunit.runner.json file, add the appDomain property.

{ "appDomain": "denied" }

Ensure the file is copied to the build output by referencing it in your test project's .csproj file with the CopyToOutputDirectory directory set to PreserveNewest or Always:

<ItemGroup>  
  <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>  

With these changes, I was finally able to get full .NET Framework tests running on Linux, in addition to my .NET Core tests. You can see an example in my NetEscapades.Configuration library, which uses Cake to build the libraries, running on both Windows and Linux using AppVeyor.

Summary

If you want to run tests of your full .NET Framework libraries on Linux, you'll need to install Mono. You must add a .props file to set the FrameworkPathOverride property, which MSBuild uses to find the Mono assemblies. You may also need to add references to certain facade assemblies. You can add them inside a Condition so they don't affect your .NET Core builds.