DEV Community

Cover image for .NET Core Apps on Linux
Bob Rundle
Bob Rundle

Posted on

.NET Core Apps on Linux

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

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

image

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

In VS Code…

image

Build and run on windows…

image

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

image

Build a Linux docker image

image

Let's try running it…

image

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>
Enter fullscreen mode Exit fullscreen mode

Now lets build and run again…

image

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

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>

Enter fullscreen mode Exit fullscreen mode

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);    
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Build the unit tests and run them…

image

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

The Dockerfile…

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

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

CMD ["dotnet","test"]

Enter fullscreen mode Exit fullscreen mode

Build and run on Linux…

image

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.

Top comments (3)

Collapse
 
sigfualt profile image
Cameron Young

“What we can agree on is 100% code coverage is the gold standard” I don’t trust 100% code code coverage on a business application. This project I have no issue with the concept but in the wild that’s almost a red flag to me.

Collapse
 
bobrundle profile image
Bob Rundle

Not sure what you mean. Indeed 100% code coverage is not achievable for many real world apps. A more reasonable target for code with a lot of UI is 60-80%. But this is only because (in my view) we don't have the toolset to do 100%. 60%-80% code coverage means that 40%-20% is tested manually which in turn means not very often or not at all.

Collapse
 
sigfualt profile image
Cameron Young

We are on the same page, I agree.