A few months ago I spent a little while looking at .NET 5, but now that 6 is out (just this week, after a little ruckus concerning “hot reload”), I decided to have another go at it. Which, if you know me at all, is kind of weird.
Despite having been at Microsoft for six years now, I should say I don’t really like .NET. Having spent way too much time doing Java, the entire object-oriented, dependency injection driven, weird abstraction overhead approach to enterprise software is something I have actively come to consider as “make work” stacked on acres of boilerplate code, and one of the main reasons I avoided “enterprise” stacks after too many rounds of broken JDKs.
.NET always felt too much like Java for comfort, and although I looked at Mono in the early days (and was quite curious about Xamarin in the mobile days), current “best practices” for .NET development actually keep me from understanding what the code is doing (what the control flow looks like, where it’s spending the most time and, crucially, why it was designed in that way).
And with software architecture and lifecycles changing so fast that most of the alleged long-term benefits of those ivory tower, heavy OOP approaches became irrelevant over time, well, I just lost interest.
And since I’ve gone down the LISP/Clojure road, even the OOP approach seems stale. For me, Go‘s take on interfaces is vastly preferable (except for weird syntax and temporary lack of generics), and I quite like the comparative sparseness of Kotlin and Swift (except, again, their weird syntax) when compared to either C# or Java.
That said, I do have a soft spot for F# (seeing as I dabbled in OCaml for a bit) and am immersed up to my eyeballs in Microsoft toolchains, so dealing with .NET is pretty much inescapable, even for a UNIX guy.
Which .NET, Then?
This was, obviously, the critical question for a good while. Another reason why I (and my erstwhile peers) shunned .NET for years was that it was a Windows-centric, constantly moving target with a “choice” of runtimes (i.e., a messy landscape), each of which with its own idiosyncrasies.
And yet, the ecosystem became massive. Even if (like me) you strongly dislike
nuget and had nightmare experiences with
$-prefixed keys in legacy schemas, C# is out there and actually makes sense to use in various scenarios I have an interest in.
For instance, it became the language for Unity development and, again, Xamarin fixed most idiosyncrasies for mobile development (and many people I still keep track of in the iOS and Android space swear by it), so I keep stumbling into C# and the broader ecosystem.
Going Hard Core
Disregarding my gut feelings and dislikes, the real issues for me have been portability and long term support. .NET Core seems to be set to fix those for everyone else, but a few months ago it wasn’t quite all there yet.
About May or so I found out that .NET Core 5.0 could build standalone binaries for multiple architectures thanks to a combination of AOT and
clang, so I decided to take a look.
Binaries were a bit on the large side (a “Hello World” build for
linux-x64 yielded 34MB for C# and 50MB for F#), but could be trimmed down using
-p:PublishTrimmed=true, which was encouraging.
Fast-forward until this weekend, and repeating that exercise for .NET 6 yielded similar results:
The differences are likely due to different base sets of assemblies and the alpha state of the AOT compiler I tried at the time, but they’re pretty much inconsequential these days–I’m not too worried about file sizes at all, really.
I’m much more interested in doing cross-platform builds, which has (so far) worked perfectly and is something the toolchain does by design.
For the record, this is how you can add the experimental AOT compiler to
nuget.config (freshly updated for .NET 6):
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <!--To inherit the global NuGet package sources remove the <clear/> line below --> <clear /> <add key="nuget" value="https://api.nuget.org/v3/index.json" /> <add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" /> </packageSources> </configuration>
And this is the
Makefile I used to set these up and build them:
bootstrap: dotnet new console bootstrap-fsharp: dotnet new console -lang f# clean: rm -rf bin obj watch: dotnet watch release-mac-m1: dotnet publish -p:PublishSingleFile=true -r osx.11.0-arm64 -c Release release-mac-intel: dotnet publish -p:PublishSingleFile=true -r osx.11.0-x64 -c Release publish-linux: dotnet publish -p:PublishSingleFile=true -r linux-x64 -c Release --self-contained publish-linux-trimmed: dotnet publish -p:PublishSingleFile=true -p:PublishTrimmed=true -r linux-x64 -c Release --self-contained
Update: I’ve since gotten an M1 Pro, and depending on when you read this you might need to use preview versions of a couple of libraries if you want to develop on
arm64. Right now that means adding these two stanzas to your
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.127" /> <PackageReference Include="SkiaSharp" Version="2.88.0-preview.127" />
Fiddling about with a few synthetic benchmarks didn’t reveal anything interesting (I’m testing these remotely on a puny little Celeron with only 4GB RAM running Ubuntu 20.04, and speed is relative in that setup), but I don’t think there will be massive performance differences in regular, I/O-bound apps.
The big difference for me is that I don’t have a gaggle of
.dll files to deploy with each app, and a slimmer footprint overall.
So for server-side stuff, this looks promising (barring my dislike of
C#), if only due to the massive library and tooling ecosystem.
While I was playing around, I decided to see if .NET could scratch one of my long-time itches: Native-looking, non-scripting-based, cross-platform GUI apps.
Yes, I know this is the web era. I don’t care, there are many situations where I prefer to have a nice, simple local app to do stuff for me with data and files on my local drive, and I have long yearned for decent ways to build native apps that run on my Mac–and elsewhere, sometimes.
Enter Avalonia. Yes, I understand there is a lot of noise about MAUI, but it’s not all there yet, and it might take a couple of years. Oh, and I am not keen on using XAML either.
But XAML is not fundamentally different from QML (I’ve been looking at Qt for a long time, and PyQt/Pyside are pretty decent until you start wading in boilerplate), and having something that demonstrably works, has loads of third-party support and a decent installed base (plus access to the entire .NET ecosystem) seems pretty neat, and Avalonia ticks nearly all the boxes.
The only thing I’m not too sold on (yet) is how well it mimics native controls using Skia, but most of the available software I’ve come across using Avalonia (like Lunacy) is very impressive, and it took no time at all to get a Hello World app going on WSL:
Avalonia can be AOT compiled too, and repeating the simple test I did for the console app yields equally acceptable results:
So I grudgingly agree .NET is a lot more interesting to me these days than it was a decade ago.
Getting things done in a terminal works fine (I’m very much a CLI and
vim developer, and every year I install a new VS edition only to toss it away a few months later unused), and that’s OK for back-end stuff.
Right now, though, there aren’t that many tools to do GUI development on a Mac, which is a shame, but a logical consequence of most of
.NET‘s enterprise IDE push. There are third party alternatives like Rider (which is by all acounts superb but not necessarily something I need), but nothing that stands out to me as a complement.
I’m likely to use .NET for something soon. Realistically, I haven’t the faintest idea of when I’ll have the time to wrap my brain around either C# or F# to the degree required (and sorely wish Clojure on the CLR was actually usable), but it’s become a when rather than an if.
The real problem, as always, is going to find that when amidst all the other stuff I’m supposed to do.