.NET Core Apps on Linux
Tags: dotnet, docker, linux
Published: 2021-05-20 02:22:17

One of the great benefits in working with .NET Core is knowing that your code will be cross platform.  In particular it will run on Linux.  This opens up a lot of possibilities.  But does it really run on Linux if you have never seen it run?  But even if you have seen it run, is it really working if you have never run the unit tests on Linux? In my way of looking at the world…no.

So in this post I will lay out how to get your cross platform .NET Core apps running and tested on Linux in the most straightforward and efficient way.

The approach is to develop code on Windows and test in Linux containers.  This is the best combination in my view.  So on your windows dev box the set up you need is Hyper-V and Docker.  Getting this setup right is not without its challenges which I will not get into here, but I am pleased to report that once you get this working it stays working and I have had this setup working for years now through all manner of Windows and Docker updates.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/brweedu7sge7y306idue.png)

Also needed are the dotnet CLI and VS Code for this optimum (in my view) setup.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9u6yaq7nijmocw9q7w9v.png)
 
All the code for this tutorial can be found at https://github.com/bobrundle/dotnettolinux

I'll start by creating a simple console app that adds the numbers that appear as arguments.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jj0w78pcsvc1uupp56bk.png)

In VS Code…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cjwpd328hk8fd9txb605.png)

Build and run on windows…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k47v6xyabgtidyo7hn66.png)

Here is where it gets interesting.  Create a dockerfile for Linux deployment…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o9ugzo0qqnvqrjlcg78s.png)

Build a Linux docker image

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vi7mu720acwqmf8asvfu.png)

Let's try running it…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x22u6957zgkz0qaogumr.png)
 
Oops.  ICU stands for Internationalization components for Unicode which is used to handle culture dependent APIs.  .NET 5.0 requires ICU by default and it is not available by default on Linux.  For a simple app such as ours, the easiest thing to do is disable globalization support.

To disable globalization support we need to add another property to our add.csproj project file…

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>

</Project>
```
Now lets build and run again…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azf33anzau163297e380.png)
 
Now let's add unit tests.  You test-first wingnuts will be very disappointed that I didn't write these first, but I am simply not a test first guy.  I could say more but need to stay focused. 

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fccwoaw6h5dq4v0hf7r4.png)
 
Need to add a project reference to add.csproj…

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

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="1.3.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  
  <ItemGroup>
    <ProjectReference Include="../add/add.csproj"/>
  </ItemGroup>

</Project>

```
Our unit tests…

```
using System;
using Xunit;
using add;
using System.IO;

namespace AddTests
{
    public class ProgramTests
    {
        [Theory]
        [InlineData(new string[] {}, "0",0)]
        [InlineData(new string[] {"1","2","3"}, "6",0)]
        [InlineData(new string[] {"1","2","a"}, "",1)]
        [InlineData(new string[] {"1.1","2.2","3.3"}, "6.6",0)]
        [InlineData(new string[] {"-1e6","1e6"}, "0",0)]
        public void MainTest(string[] args0, string r0, int e0)
        {
            string outfile = Path.GetTempFileName();
            var outstream = File.CreateText(outfile);
            Console.SetOut(outstream);
            int e1 = Program.Main(args0);
            Console.Out.Close();
            string r1 = File.ReadAllText(outfile);
            Assert.Equal(e0, e1);
            if(e0 == 0)
            {
                Assert.Equal(r0 + Environment.NewLine,r1);    
            }
        }
    }
}

``` 
Build the unit tests and run them…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8p2goo11142upz14brpi.png)

To run the unit tests in Linux we need to more than move binaries…we have to setup a development environment and build the code before running the tests.  To do this we need a Docker file in the parent directory to both code and test folders.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ys831r34p98q0lrxvji.png)

The Dockerfile…

```
FROM mcr.microsoft.com/dotnet/sdk:5.0

WORKDIR /src
COPY /add add
COPY /addtests addtests
WORKDIR /src/addtests

CMD ["dotnet","test"]

```
Build and run on Linux…

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wjqjudhkuhy6ffcrkp9.png)

## Summary and Discussion

To recap:

1. A simple Windows console app was created, built and run on Windows.
2. The console app was built for Linux on Windows and run in a Linux container.
3. A xUnit testing library was created to run tests against the console app.  It was built and run on Windows.
4. The source for both the console app and the xUnit tests were built and run in a Linux container.

The following questions about this approach come to mind...

**Why are you not a test-first guy?** My answer is too long to be considered here.

**Your "unit tests" are actually integration tests!** This is semantics. What we can agree on is 100% code coverage is the gold standard of automated testing and this has been achieved in this example.

**What about macOS?** You cannot run macOS containers on Windows.  You can only run macOS containers on Macs.  There might be a way to test all 3 platforms (Windows, Linux, macOS) on a Mac with containers.  I will experiment when I get a chance.

**Why build in the Linux container?  Why not simply use a test runner to run the binaries?** Indeed, this is a good idea.  I simply don't know how to get this to work with xUnit.

**Why not construct a CI/CD pipeline to build and test on Windows and Linux in the cloud?** Indeed, the next logical step. However, you still cannot reach macOS in the cloud.

I hope what I have done is useful and addresses some questions you might have.  I spent about a day researching the various aspects of this problem. I came to this issue when I was designing a command line tool for Windows and came to realize that the tool would be useful on Linux.  Then I began to look into building and testing on Linux and discovered the approach was not well documented and not straight-forward and so suggested a post to capture the learnings.