Linux .NET Development in 2019
What you need to know
I’ve recently been building .NET Core back-end services that run on Linux. Linux .NET development is in an interesting place; it’s clearly the future of back-end .NET, but it’s still a little rough around the edges compared to our old friend .NET-on-Windows.
Let’s dive into what you (an experienced Windows .NET developer or a .NET-curious Linux developer) need to know to start building .NET services for Linux. I’ll cover IDEs, service hosting, Linux system calls and more.
Background & Motivation
.NET development for Linux has been possible via Mono since 2004, but it was always a bit… fringe compared to Windows .NET development. That all changed when Microsoft released .NET Core in 2016 as a cross-platform .NET implementation; first-party support from Microsoft is a big deal to most .NET developers.
We’re also in a world where Linux is the lingua franca for back-end development; if you want to do anything involving cloud services, distributed systems, or containerization, Windows is typically an afterthought (if it’s supported at all). I want to skate to where the puck is going (sorry!), and it’s headed toward Linux.
Which IDE should I use?
You can eschew an IDE and do everything with the dotnet
CLI tool, but personally I want more tooling. There are 4 big IDEs for modern .NET development:
- Visual Studio for Windows
- Visual Studio Code (Windows, Linux, Mac)
- Visual Studio for Mac
- JetBrains Rider (Windows, Linux, Mac)
The best choice for Linux development will depend on your personal OS and the specifics of your application.
Visual Studio for Windows is the one to beat, it’s got excellent .NET tooling. However, it doesn’t have a great story for remote Linux .NET development; to test Linux-specific functionality, you’ll need to deploy to a remote system after compiling. Not the end of the world, but I prefer a tighter edit-compile-debug loop. Interestingly, VS support for remote Linux C++ development has seen a lot of investment recently; it’s a safe bet that this feature will come to .NET eventually.
VS Code supports remote development on a Linux server, and it’s a killer feature. I just point VS Code at my Ubuntu VM, and bam, it’s like I’m on a Linux workstation. Debugging and file browsing just work. The downside is that VS Code’s C# tooling is much less mature than “real” VS. I need to restart the OmniSharp language server multiple times a day (Microsoft employees complain about this too!), and you’ll need to fall back on the CLI frequently.
VS for Mac is the descendant of Xamarin Studio. It has much better .NET tooling than VS Code, but no remote development feature. I’m on a Mac and I still prefer VS Code most of the time, but YMMV.
JetBrains Rider runs natively on Mac, Windows, and Linux. I’m told that it’s a great choice if you run Linux as your main OS, but it doesn’t have a remote development feature.
Systemd Services
If you want a long-running service on Windows, typically you host your code in a Windows Service. The equivalent on Linux (for most distros) is a systemd
service. It’s really easy to write a systemd
service in .NET Core using the Microsoft.Extensions.Hosting.Systemd package:
- Host your application using the .NET Generic Host (HostBuilder)
- The
dotnet new worker
template is the easiest way to do this from scratch
- The
- Add the Microsoft.Extensions.Hosting.Systemd NuGet package to your project
- Call the
UseSystemd()
extension method on your HostBuilder - Create a systemd unit file
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
Once you do that, the service acts just like any other systemd service; manage it with systemctl
, view its logs with journalctl
, etc.
Microsoft.Extensions.Hosting.Systemd isn’t magic, and it’s surprisingly easy to implement your own systemd
layer if you need to; under the hood that library is just making a Linux syscall, reading from /proc
, and writing to a Unix Datagram socket.
Syscalls
Sometimes you want to do platform-specific things, right? Good news, that’s (usually) easy.
You can make syscalls directly using P/Invoke. Here’s how to call getpid to get the current process ID:
// leave EntryPoint off if you want it inferred from the name of the C# function
[DllImport("libc", EntryPoint = "getpid")]
private static extern int getpid();
However, most of the time you’ll want to use the Mono.Posix.NETStandard package instead. It handles the thankless work of wrapping native calls safely. Don’t be turned off by the Mono namespace; Mono.Posix.NETStandard is the current Microsoft-blessed way to do syscalls.
Update: the new NativeLibrary class in .NET Core 3 is a handy alternative to P/Invoke that can be configured at runtime.
Miscellaneous
Sometimes you’ll run into situations where common .NET APIs don’t handle *nix features well. For example, the .NET I/O APIs weren’t designed for symlinks:
FileInfo does not resolve symbolic links, leading to subtle bugs. There’s no API that gives information about the link target.
I expect that this will get better as .NET development for Linux becomes more mainstream.