This series is a tutorial for a friend of mine. He has need to run code that was written against .NET 9 or 10 on something other than Windows.
While that code can be run naively on macOS or a Raspberry Pi, the goal is to show code running in a Docker container that pulls in a NuGet package.
In this article, we'll look at running .NET code natively on macOS; the process for running .NET on Linux or a Raspberry Pi should be very similar.
In the next post, we'll also discuss containerizing this project with a multi-stage Docker build.
Code for the final application as well as the multi-stage Dockerfile can be found on GitHub
Building "Hello World!" in C# against .NET 10 on macOS
For our first trick, we'll get the classic "Hello World!" in C# working macOS Golden Gate 27.21.
Installing .NET
First, we need to download and install .NET2. The official download site can be found here, but .NET is also available via Homebrew3.
Once .NET is installed, and your environment has been updated4, you should have the dotnet command available to you, try
dotnet --version to prove your shell is set up and ready to go.
Building "Hello World!"
Run the following command:
dotnet new console -n HelloWorld This will create a directory called HelloWorld which will contain a Program.cs file, an obj directory with object files inside it and a HelloWorld.csproject file.
The entirety of the Program.cs file is a single line:
Console.WriteLine("Hello, World!"); To build the project, run the following command:
dotnet build Running "Hello World!"
To run the program, cd into bin/Debug/net10.0
cd bin/Debug/net10.0 and then execute
dotnet HelloWorld.dll From the screenshot above, you may have deduced that HelloWorld is an executable file because its name is in red. In fact, running file on HelloWorld tells us HelloWorld is indeed a Mach-O 64 bit executable arm file:
And you can execute it:
Running file on HelloWorld.dll gives us PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows5.
It turns out that the Microsoft preferred way of executing .NET applications is to execute the .dll, not call the executable directly.
Building a .NET application with a NuGet package dependency
First, we'll replace the single line of code with the following:
using System; using Newtonsoft.Json; // Your original Hello World Console.WriteLine("Hello World!"); // New JSON code var data = new { message = "Hello World!" }; string json = JsonConvert.SerializeObject(data, Formatting.Indented); Console.WriteLine(json); To build and run the project with one command execute the following:
dotnet run Oh noes! We have an error!
Adding a NuGet package to the project from the command line
Actually, that was intentional; I wanted to demonstrate what it looks like when you don't add the NuGet package to the project. Execute the following command to add the Newtonsoft.Json NuGet package to the project:
dotnet add package Newtonsoft.Json Now dotnet run will build and execute our .NET program:
Not very exciting, but it works.
In Part 2, we'll discuss a multi-stage Docker build for this project.
This is a value for value blog. If you got any value out of this post, please translate it to a number and send it back to me:
-
I assume that what I'm about to show will work on other, older versions of macOS, but that was the version of macOS I was running when I attempted this feat prior to documenting and it's the only version of macOS that I have access to. ↩
-
My friend and I were having trouble communicating versions of ".NET" early on thanks to Microsoft's very not-clear naming convention. .Net Framework was the original name. Then came .NET Core where Microsoft attempted to standardize .NET and make it cross platform for Windows, OS X and Linux. Staring with version 8, the nomenclature is now just ".NET" or ".NET 8", ".NET 9" or ".NET 10". ↩
-
.NET is also available via Homebrew for (Raspberry) Pi OS. Microsoft recommends using their install script by piping it right into your command line a la
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel LTS; of course that's a very, VERY safe operation. (Please note the dripping sarcasm.) ↩ -
You may have to
execyour shell for any open tabs to get the shell to re-read your.*rcfile e.g..zshrcExample command:exec zsh. ↩ -
Keep in mind that this file doesn't really execute; it's the .NET interrupter that executes. What's really inside the
.dllis Common Intermediate Language (CIL). If a 64-bit environment isn't explicitly targeted, the default wrapper for CIL inside a.dllforAny CPUisPE32NOTPE32+. ↩













