.NET has been taking huge leaps and bounds in the last few years, and not everyone is aware of it! We’re now able to build small fast single-file .NET applications; this is arguably Go’s biggest strength, and now you can do it in C# and F# too.

Meanwhile, the .NET community has been making excellent libraries to make console applications slick, polished, and easy to write.

The conditions seem right for a .NET renaissance of sorts; all the pieces are in place for us to build great CLI-first software. Here are some useful+easy things you can do in .NET today:

Publish Small Zero-Dependency Executables

In .NET 6 it’s easy to build your application as a single file with no external dependencies (even the .NET runtime!). Applications that include their own runtime are called self-contained. Self-contained apps can be trimmed to remove unused code; trimming reduces a Hello World application from about 60MB to 12MB.

Trimming is well-supported by nearly the entire standard library. Ecosystem support varies; if a library makes heavy use of reflection, C++/CLI, or COM interop then it might not work with trimming yet.

The easiest way to build a trimmed self-contained single-file executable is dotnet publish with the args --self-contained=true -p:PublishSingleFile=true -p:PublishTrimmed=true.

dotnet-releaser makes publishing your app a breeze; it can build for multiple platforms+architectures, then publish the resulting artifacts in a GitHub release.

Console UI

Spectre.Console is a great replacement for Console.WriteLine() and friends. Coloring text with Spectre is as simple as:

AnsiConsole.MarkupLine("[blue]Hello[/] [yellow]World![/]");

It can also do much more; if you need to print tables or prompt users for input, Spectre.Console has you covered. But if you need to build a full-blown terminal UI, check out gui.cs.

Running External Processes

The System.Diagnostics.Process APIs built into .NET are clumsy, to say the least. Thankfully we have much better options these days! Here’s how to run git status with the excellent SimpleExec:

var (stdout, stderr) = await RunAsync("git", "status");

For a more powerful solution, check out CliWrap; anything you can do in a Bash script, you can do in CliWrap.

I recently spent a bit of time messing around with the Postgres wire protocol. I’d like to build a library that lets any application accept connections as if it were a Postgres database; many database clients and databases speak the Postgres wire protocol, and it seems like a practical way to expose tabular data over the network.

Implementing the protocol isn’t too bad; the docs aren’t great but the protocol itself is relatively straightforward. Unfortunately, client behaviour seems harder to accommodate. Clients can issue arbitrarily complex SQL queries to the database to inspect its schema, and sometimes they expect accurate answers. For example, Npgsql issues this beast and then disconnects if it gets an invalid response.

So I’m stuck with a question: how far should I go to support legitimate client behaviour outside of the protocol? The easiest approach for now is to test with a few popular clients and hardcode responses for any queries that they need implemented, but that will leave a long tail of unsupported clients. On the other end of the complexity scale, I could try running schema queries on an in-memory SQLite instance with database objects that mimic Postgres (certainly a massive yak shave, but tempting nonetheless).

I think my next step will be to dig through the source of a few Postgres-compatible databases to see how they solve this.

I recently assembled a tool:

ScriptCompiler screenshot

It’s a helper for scripting in C#. It watches a directory with C# files for changes, and whenever a file changes it gets compiled into its own executable. You can install it as an always-on service, and it comes with an opinionated set of utilities to make scripting easier.

Why?

C# the language has recently become much more succinct (top-level statements, global usings); seems like it should be good for writing fast, maintainable scripts! But the tooling around the language still isn’t quite there yet; you need a .csproj project file for every program, and dotnet run often takes a second or 2 to start up, it’s not (yet) optimized for scripting use. This proposal should help in many ways (upvote it!) but who knows if or when it will be done.

Also, this has been stuck in my head for a while:

My OS has no notion of build system. It has all the source code on it and I can edit anything and run the command again with the change immediately applied. Interpreter or compiler, I don’t know, that’s an implementation detail. Then I wake up.

Give it a spin

Instructions, source and pre-built executables for Linux+macOS+Windows are available on GitHub.

I had a weird week back in October, and it all started when I posted this GitHub issue.

What happened?

The .NET team built a feature called Hot Reload for .NET 6, which would allow for code to be changed while an application is running. It was planned as a feature for all of .NET from the start:

Our goal is to provide a consistent dev inner loop experience in .NET Core across all platforms, architectures, and workloads.

A preview version of the feature landed in April 2021 and by October it was pretty much done. I used Hot Reload throughout the preview period and loved it. Then at the last minute, Microsoft announced that the feature would be locked to Visual Studio; to say I felt disappointed would be an understatement. I posted on GitHub asking for more information, and I soon found out that many, many others felt the same way.

There was a huge community uproar, I was quoted in multiple news articles, and Microsoft eventually backed down. It was a happy ending, but I was still left with a bitter taste in my mouth.

OK, so what?

To many people, it’s not obvious why this was such a big deal. I think there are 2 main reasons:

  1. Quick feedback isn’t a niche feature; it’s an essential element of any creative activity.
  2. Limiting Hot Reload to Visual Studio would exacerbate .NET’s existing weaknesses.

I’ve rambled about 1 before, so I’ll focus on 2 here.

.NET’s at a bit of a disadvantage when it comes to cross-platform development, in both perception and reality. The tooling situation outside of Visual Studio (or Rider) often leaves much to be desired, and newcomers often get a poor first impression from the OmniSharp tooling in VS Code. Hot Reload in dotnet watch is a big step forward for .NET outside of Visual Studio; it’s a feature I can point to and say “yes, .NET has good tooling no matter where you use it.”

That’s the crux of the matter; removing Hot Reload from dotnet watch would harm .NET in an area it needs to do much better in. The .NET team understands this, but the change was imposed on them by senior Developer Division leadership against strong internal opposition. This suggests that senior leadership does not understand .NET (bad) or prioritizes Visual Studio over the long-term health of .NET (worse).

I like working in .NET, and the .NET team is doing a world-beating job of pushing the ecosystem forward from its stodgy enterprise-first origins. It’s unsettling that senior Developer Division leadership was willing to throw so much away to sell a few Visual Studio licenses.

headshot

Cities & Code

Top Categories

View all categories