My blog about software development on the Microsoft® stack.

Platform Conditional Compilation in .NET Core

.NET Core 1.0 introduced the System.Runtime.InteropServices.RuntimeInformation type that lets you dynamically detect which operating system (OS) your app currently runs on. You do this by calling the static IsOSPlatform method at runtime and passing in any of the supported System.Runtime.InteropServices.OSPlatform enumeration values like this:

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
    Console.WriteLine("Running on Linux!");

If you want to perform this check at compile time in order to build certain parts of your code only for a specific OS, you can do this using conditional compilation directives:

#if Linux 
Console.WriteLine("Built on Linux!"); 
#elif OSX 
Console.WriteLine("Built on macOS!"); 
#elif Windows 
Console.WriteLine("Built in Windows!"); 
#endif

You define these directives in the .csproj file. Right-click on the project in the Solution Explorer in Visual Studio and select “Edit *.csproj”, or open the project file in a simple text editor such as Notepad, and add a child element per supported OS to the existing <PropertyGroup> element:

<PropertyGroup> 
  <OutputType>Exe</OutputType> 
  <TargetFramework>netcoreapp2.0</TargetFramework> 
  <IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows> 
  <IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX> 
  <IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux> 
</PropertyGroup>

The Condition of each element checks whether the result of calling the IsOSPlatform method for the respective platform at build time equals to true.

You then add another <PropertyGroup> element for each of the platforms to define the actual directives to be used in the source code. The full contents of the .csproj file will look like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
    <IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
    <IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
  </PropertyGroup>
  <PropertyGroup Condition="'$(IsWindows)'=='true'">
    <DefineConstants>Windows</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(IsOSX)'=='true'">
    <DefineConstants>OSX</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(IsLinux)'=='true'">
    <DefineConstants>Linux</DefineConstants>
  </PropertyGroup>
</Project>

The goal here is to have the following minimal console application printing out “Built on Linux” (only) when the code was compiled on a Linux machine.

class Program
{
    static void Main(string[] args)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            Console.WriteLine("Running on Linux!");
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            Console.WriteLine("Running on macOS!");
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            Console.WriteLine("Running on Windows!");

        #if Linux
        Console.WriteLine("Built on Linux!"); 
        #elif OSX
        Console.WriteLine("Built on macOS!"); 
        #elif Windows
        Console.WriteLine("Built in Windows!"); 
        #endif
    }
}

You can verify that this works by installing a Docker client on your computer. If you are developing on a Windows 10 machine, you install the Docker for Windows desktop app. Instructions on how to do this can be found in the official Docker documentation. There are client apps available for both macOS and several of the most common distributions of Linux as well.

Once you have successfully installed and started Docker for Windows you should be able to run Docker commands from a command prompt. The docker build command builds an image from a Dockerfile. A Dockerfile is nothing but a plain text file that contains a set of instructions that makes up an executable package that contains everything needed to run an application. This includes not only your custom code but also the runtime and everything else down to and including the underlying operating system.

The following Dockerfile tells Docker to pull down an image that contains the latest version of the .NET Core SDK, including the command line tools (CLI) that are required to be able to build the app. This image is provided and maintained by Microsoft and stored in a repository of a cloud-based registry service called Docker Hub. There are several other official .NET Docker images available on Docker Hub. Each one targets a specific .NET or OS version or a specific deployment scenario. There are for example different and optimized images available depending on whether your focus is on developing apps, build them or simply run already built apps. In this particular case I want to build the app on Linux which is why I choose the SDK image.

#pull down the Docker image that contains the latest version of the .NET Core SDK, including the Command Line Tools (CLI) 
FROM microsoft/dotnet:sdk 
#Sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions in the container 
WORKDIR /src 
#copy the project into the working directory 
COPY . ./ 
#build the app and add the output to a folder called "release" 
RUN dotnet publish -c release -o release 
#change the current working directory to app/release 
WORKDIR release 
ENTRYPOINT ["dotnet", "DemoApp.dll"

The microsoft/dotnet:sdk image is multi-arch which means that it will install the .NET Core SDK on either Windows or Linux depending on what mode is set in Docker for Windows. By default, Docker for Windows uses Linux containers. You can switch to using Windows containers by right-clicking on the Docker icon in the notifications area down in the bottom-right corner of the screen and select “Switch to Windows containers”. If you do this and build and run the sample app using Docker, it will indeed print out “Built on Windows!” as the Docker daemon will use the multi-arch image to automatically figure out what base image (OS) to pull down.

Docker images are layered. This basically means that an image is based on another image which in turn may be based on yet another image and so on. If you go to GitHub and look at the definition of the Docker file that the microsoft/dotnet:2.1-sdk image is built from on Windows, you will see that the first thing it does is to pull down a microsoft/windowsservercore:1803 image. This image contains the 1803 version of the Windows Server Core base OS that the microsoft/dotnet:2.1-sdk image then installs .NET Core on.

microsoft/dotnet:sdk (currently as of the time of writing this in early November 2018) refers to the exact same image as microsoft/dotnet:2.1-sdk by the way. This is controlled by tags. Microsoft list the complete set of available tags on their GitHub repo. If you take a look there you can see that the 2.1-sdk, sdk and latest tags currently refer to the same image, i.e. regardless of whether you write FROM microsoft/dotnet:sdk, FROM microsoft/dotnet:2.1-sdk or FROM microsoft/dotnet:latest in the Dockerfile, Docker will pull down the exact same image. This holds true until there is a newer version of .NET Core available. Then the sdk and latest tags will bring down that new version while using the 2.1-sdk tag will still get you the 2.1 version of the SDK.

Besides installing the SDK to be able to build the application, the above Dockerfile uses the WORKDIR instruction to set the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that will follow. This is a relative path within the Docker container. It then copies the contents of the current folder – this is the folder on your hard drive where your source code is located – into the container’s working directory using the COPY instruction. After this it uses the .NET Core CLI to build the application. The --output (-o) option specifies the path for the output directory. Finally, it switches the working directory to the output directory where the newly compiled assemblies are located and starts the .NET Core application using the ENTRYPOINT instruction.

Let’s try this out. The first thing you should do to is to add the Dockerfile to the project in Visual Studio. Once you have added the file to the root project folder where the .csproj file is located, you can start a command prompt and run the following docker build command:

>docker build -t demoappimage C:\Users\magnu\Documents\DemoApp 

C:\Users\magnu\Documents\DemoApp is the folder where the project file and the source files are located. If you don’t specify a filename using the --file (-f) option, the command will lock for a Dockerfile named Dockerfile without any file extension in this directory. The --tag (-t) option lets you specify a name for the image. There are several other options that are listed in the documentation.

Docker build
Once the command has finished downloading and extracting all dependencies and executed the instructions in the Dockerfile, you should now have an image. You can confirm this by running the docker images command. This command lists all images that Docker currently knows about:

docker images
A Docker image itself can never be started nor executed directly. It’s just a template. You use the docker run command to produce a runnable container based on an image. The following command uses the “demoapp” image created in the previous step to create and start an isolated container process named “demoappcontainer” that actually runs the sample app:

>docker run --name demoappcontainer demoappimage 

docker run
And as you can see, “Built on Linux” is printed to the console. This means that the conditional compiling directives are working as expected. If you switch to using Windows containers and create a new image and a new container, you should see “Built on Windows” being printed to the console. Before you do this, you might want to remove the existing container using the docker rm command:

>docker rm bb05b7384f09 

The argument specifies the id of the container. You can provide the full id as listed by the docker images command, or as many characters of the id that are needed to be able to distinguish it from another container’s id. For example, if you have a container with an id of “21a” and another one with an id of “22b”, you can remove the first one using the argument “21” as there is only one container that has an id that starts with “21”. The docker rmi command that removes an image works the same way.

Using Docker is not the only way to verify that the conditional compilation directives work as expected on Windows 10. You may also use the built-in Windows Subsystem for Linux to do this. This is arguably even easier.
The first thing you do is to enable the subsystem using the following Powerhell command:

>Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 

You can also enable this feature in the Control Panel under Programs->Turn Windows features on or off.
Once you have done this, you can install a Linux distribution of your choice from the Microsoft Store for free. Open the Microsoft Store app and search for an install the “Ubuntu” app for example.

Store
Once you have installed and launched this app, you will eventually be prompted to provide a Unix username and password to create your LINUX user account:

Ubuntu
Once your account has been set up, you can then install the .NET Core SDK onto the Linux subsystem using the commands listed here.

~$ wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb 
~$ sudo dpkg -i packages-microsoft-prod.deb 
~$ sudo apt-get install apt-transport-https 
~$ sudo apt-get update ~$ sudo apt-get install dotnet-sdk-2.1 

Then you just copy the source code over and build the app using the SDK and the following commands:

~$ cp -r /mnt/c/Users/magnu/Documents/DemoApp ~/DemoApp 
~$ dotnet publish ~/DemoApp -c release -o ~/DemoApp/release 
~$ dotnet ~/DemoApp/release/DemoApp.dll 

You should once again see “Built on Linux!” being printed to the console when your app is run:

Linux
As we have seen, it’s an easy thing to compile for different platforms in .NET Core. You simply define the directives you want to be able to use in your source code in the project file and then use them like you would use any other preprocessor directives. Docker and Windows 10 also makes it easy to develop, test and run cross-platform .NET core applications.


4 Comments on “Platform Conditional Compilation in .NET Core”

  1. Awesome post thanks for sharing this. I am a newcomer to Dot net developer thanks for the informational article. extremely enjoyed reading it all.

  2. Thanks for this example! It took way too much Googling to find this answer.

  3. Abhishek Shrivastava says:

    Bingo… great work..

  4. Thank you! This is one of the many examples where the insane level of configurability is sometimes overkill for us non-Windows users of .NET… we’re used to having the compiler generating all these fancy tags, not the human user (extracting information from the assembly as you did). But Microsoft’s approach gives much more flexibility (yay! defining our own compiler tags!).

    Also note that this is working well under .NET 6 (and possibly will also work on 7… not tested yet!)


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s