Platform Conditional Compilation in .NET Core
Posted: November 5, 2018 Filed under: .NET Core | Tags: .NET Core, Docker 4 Comments.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.
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:
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
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.
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:
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:
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.
Awesome post thanks for sharing this. I am a newcomer to Dot net developer thanks for the informational article. extremely enjoyed reading it all.
Thanks for this example! It took way too much Googling to find this answer.
Bingo… great work..
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!)