Getting Started with xUnit.net

Multi-targeting with non-Windows OSes

This article describes the process that is necessary to enable multi-platform, multi-target builds; specifically, it's designed for users who are attempting to enable multi-targeting of .NET Framework when running on Linux or macOS. It leverages the .NET SDK command line tool (dotnet) to do builds and test execution.

This documentation will include links and version numbers that were valid at the time it was created. Newer package versions and links may become available in the future, so your results may be slightly different.

Pre-Requisites

The requirements to support multi-targeting on Windows and non-Windows OSes are different. For Windows OSes, it is assumed that the (one and only one version) of .NET Framework will be installed as part of your Windows installation, so the only required step is to enable it. The build system for the .NET SDK already understands how to build .NET Framework applications on Windows, and dotnet test already understands how to run them.

On non-Windows OSes, you're missing two key components: the libraries that are required to compile your applications, and the .NET Framework runtime. The former are added (in a somewhat convoluted way) through NuGet packages, and the latter comes from Mono: an open source implementation of the .NET Framework that's available for Linux and macOS (the other two officially supported platforms for the .NET SDK).

So, in addition to installing the .NET SDK, you will also need to install Mono.

Find .NET Framework Reference Package

Unlike most NuGet packages, the process of enabling .NET Framework support for your multi-targeted projects is not just a matter of adding a single package. Instead, the process is split up into two steps: identifying the package with your libraries, and updating the .csproj file to include the directives to allow the C# compiler to find the .NET Framework reference libraries when building your executable.

As of the writing of this article, these libraries (created by the .NET team at Microsoft) are housed on MyGet, rather than the more traditional NuGet repository. In order to find the package you want, you need to visit the .NET Core Gallery on MyGet and find your framework's package version.

On the gallery page, go to the Search box and type microsoft.targetingpack.netframework. This will limit the package list to those packages that provide the reference libraries for the .NET Framework. Find the one that matches the version of the .NET Framework that you're targeting, and note it's version number. (For our example, we'll be using package Microsoft.TargetingPack.NETFramework.v4.5.2 which is currently version 1.0.1.)

Updating Your .csproj File

There are three (or four) changes to make to your .csproj file to enable multi-targeting, supporting both Windows and non-Windows machines.

Setting TargetFrameworks

Make sure you've updated your <TargetFrameworks> element to include the .NET Framework version you're planning to target. Note that if your project was previously single-targeting .NET Core, you will need to change <TargetFramework> (singular) to <TargetFrameworks> (plural). In our example, we're adding net452 to our existing netcoreapp2.1 test project:

<PropertyGroup>
  <TargetFrameworks>net452;netcoreapp2.1</TargetFrameworks>
</PropertyGroup>

Adding NuGet package reference

You will need a NuGet package reference for the .NET Framework reference libraries. You will add this to a conditional <ItemGroup> inside your .csproj file. For our example, the item group looks like this:

<ItemGroup Condition=" '$(OS)' != 'Windows_NT' AND '$(TargetFramework)' == 'net452' ">
  <PackageReference
      Include="Microsoft.TargetingPack.NETFramework.v4.5.2"
      Version="1.0.1"
      ExcludeAssets="All"
      PrivateAssets="All" />
</ItemGroup>

You will replace the exact package name and version with the one you found in the step above. Since the condition is gated on both non-Windows OSes as well as the specific target framework, you can add more of these as needed for each .NET Framework target your project includes.

Adding compiler settings to find the reference libraries

Once we have the package downloaded, we need to inform the C# compiler where the framework reference assemblies live. We also need to let NuGet know where the NuGet package lives, since it's currently only available on MyGet. For our sample, this means adding a new <PropertyGroup> element with these two values:

<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' AND '$(TargetFramework)' == 'net452' ">
  <FrameworkPathOverride>$(NuGetPackageRoot)microsoft.targetingpack.netframework.v4.5.2/1.0.1/lib/net452/</FrameworkPathOverride>
  <RestoreAdditionalProjectSources>https://dotnet.myget.org/F/dotnet-core/api/v3/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>

Once again, you will replace your package name and version number as needed. Note that the NuGet package cache always stores package names in lowercase, and since non-Windows OS file systems can be case-sensitive, you too must use fully lowercased names.

Adding references to system libraries (as needed)

Normally there is a batch of system libraries that automatically get referenced when building .NET Framework applications. You may find as you build on non-Windows OSes that you're missing some of these references that might otherwise have been included automatically on Windows. In our example, we were missing one reference:

$ dotnet build
Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 30.82 ms for ~/src/MyFirstUnitTests/MyFirstUnitTests.csproj.
  MyFirstUnitTests -> ~/src/MyFirstUnitTests/bin/Debug/netcoreapp2.1/MyFirstUnitTests.dll
Class1.cs(7,10): error 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, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a'. [~/src/MyFirstUnitTests/MyFirstUnitTests.csproj]

You can add them as <Reference> elements in the <ItemGroup> you created when adding your NuGet packages, since it already contains the conditional statement. To fix our error above, we needed to add one reference:

<ItemGroup Condition=" '$(OS)' != 'Windows_NT' AND '$(TargetFramework)' == 'net452' ">
  <PackageReference
      Include="Microsoft.TargetingPack.NETFramework.v4.5.2"
      Version="1.0.1"
      ExcludeAssets="All"
      PrivateAssets="All" />
  <Reference Include="System.Runtime" />
</ItemGroup>

Note that non-Windows OSes, file systems are often case sensitive, so when adding references, make sure the names exactly match the casing of the files on your file system (without the .dll extension). You can find those files in your NuGet cache folder. For our example, we found the files here:

$ ls ~/.nuget/packages/microsoft.targetingpack.netframework.v4.5.2/1.0.1/lib/net452
Accessibility.dll				    System.Net.NetworkInformation.dll
CustomMarshalers.dll				    System.Net.Primitives.dll
ISymWrapper.dll					    System.Net.Requests.dll
...
System.Net.dll					    WindowsBase.dll
System.Net.Http.dll				    WindowsFormsIntegration.dll
System.Net.Http.WebRequest.dll			    XamlBuildTask.dll

One file that constantly causes trouble is System.Xml. The automatically generated reference is for System.Xml, but the file on disk is named System.XML.dll. When you add your <Reference>, make sure you use the properly cased name. This is one reason why verifying the filename on disk is critical when adding any references. It's also valuable to make sure you've tried your build on a case-sensitive file system (for example, using the default ext4 on Linux).

Running Tests

If all of these steps have been performed successfully, then your normal command line tools should all run successfully: dotnet restore, dotnet build, and dotnet test. Give them a try! Here are a few tips:

As previously mentioned, when running on non-Windows OSes, dotnet test knows how to launch your .NET Framework tests with Mono. If all goes to plan, when you run your tests, you should be able to successfully run your .NET Framework tests. You may even see signs of Mono in your stack traces:

Assert.Equal() Failure
Expected: 5
Actual:   4
Stack Trace:
  at MyFirstUnitTests.Class1.FailingTest () [0x0000a] in <ea38f081094e407290795149a3e20d66>:0
  at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke(System.Reflection.MonoMethod,object,object[],System.Exception&)
  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x0003b] in <7b0d87324cab49bf96eac679025e77d1>:0

Happy testing!

Copyright © .NET Foundation. Contributions welcomed at https://github.com/xunit/xunit.github.io.