C#: 6 Years Later

6 years ago, I wrote a post titled “6 Months with C#” to summarize my experience with the language. Since then, I haven’t programmed in C# professionally (I did do quite a few C# programming with my own hobby games using Unity and Godot).

Recently, I’ve picked up C# again for a work related project. The language (and its ecosystem) has changed a lot during the past 6 years and I’ve dropped down some quick notes of my experience with C# this time, just as a quick comparison to 6 years ago.

New Language Features

6 years ago I was writing C# 4.0 (.Net Framework 4). async/await was introduced in C# 5.0. My first experience using async/await is actually in in JavaScript. Both implementations are straightforward. And support has been expanding in the language (e.g. C# 7.1 allows async Main function). With the feature introduced a while ago, most of the C# libraries have caught up and provided decent API support.

Here’s a list of new features I find myself using quite a lot

String interpolation

Null-conditional operator: data?.value (Although I prefer more explicit way of handling null like Option<T> , null has been the core of the language so this syntax sugar is very nice)

(Although I prefer more explicit way of handling null like , null has been the core of the language so this syntax sugar is very nice) Pattern matching

Auto property initializers: public int Speed { get; set; } = 5;

Out variables: Int32.TryParse("1024", out int number);

Expression in function members: public string Speed => 5";

Tuple enhancements: no more ugly tuple.Item1 and tuple.Item2

I discover most of these new features by IDE telling me if things can be done with a newer and cleaner way. I will talk about IDE later.

.NET Core

In my previous post, my biggest complaint is that “C# is Windows only” - Mono was never a production grade runtime. With .NET Core, it is theoretically possible to “write Windows and run on Linux”. However, in practice, it still depends:

One of your dependencies does not work cross-platform. This is less of a problem for server-side libraries. A typical example is WPF: if you have a WPF-based UI app, don’t expect to work on Linux any time soon.

If your code interpolate with native (ABI or OS-specifics), then you will have to take care of that yourself

If you have a DLL compiled with .NET Framework, even if the code is “compatible”, the same DLL will not work with .NET Core. You will have to recompile it with .NET Standard or use multi-targeting.

Some of the API does not behave uniformly across platforms. An example would be Process.PrivateMemorySize64 always returns 0 on Linux. My biggest complaint is that this is not really mentioned in the documentation.

The C# project I am working on requires taking the game logic code written in .NET Framework 3.5 (Unity), making it Unity-independent and run in on a Linux server. Most of the C# works works “out of the box” when moving from .NET Framework 3.5 to .NET Core 2.2. There are some Unity related quirks that I have to deal with, some DLLs needs to be recompiled. Overall it has been a smooth ride.

Develop on Mac/Linux

My work laptop is a Macbook Pro. So when I start to work on the project on macOS. Six years ago, Visual Studio (+ Resharper) was pretty the only viable option if you want a powerful IDE. However, since Jetbrains introduced Rider, it has become a very competitive alternative. Furthermore, VSCode (+ Omnisharp) has come a long way. I use it for my own C# game projects and am pretty happy with it. And there’s Visual Studio for Mac, an evolved Monodevelop.

Overall, there’s no problem writing C# on non-Windows. However, I ended up installing Windows on my Mac and switch to Windows. The reason?

There are no good profilers on Mac/Linux for C#. I used Jetbrains dotTrace and Red Gate ANTS: neither works on Mac/Linux. In fact, since most developers writing C# use Windows, there’re no motivations for these tools to support Mac/Linux.

Some internal tools are Windows-only. For example, the game settings’s tool is a WPF application, which unfortunately does not work on Mac/Linux

Deploy and Run on Linux

.NET Core comes with self-contained deployment, which makes it very easy to deploy on Linux. Whether you use machine images (like AMI) or containers, you can produce a self-contained binary with your CI pipeline and copy the executables over to the target to produce deployment image.

Six years ago, you could in theory deploy on Linux via Mono. However, Mono had some pretty noticeable performance issues compared to .NET Framework on Windows. So this time I decide to run a series of load testing for the .NET Core project (a UDP game server) on Linux (on a micro AWS EC2 instance running Ubuntu). The result has been quite good: the code is performant and there’re no serious GC issues.

Ecosystem

Six years ago, I complained that .NET ecosystem is too Microsoft-centric. Six years later, this statement is probably still more or less true but the difference is that Microsoft is more “open”. Six years ago, the C# ecosystem was used to tie developers to the other (expensive) Microsoft platforms and solutions. Six years later, you can feel that Microsoft provides support and guidance to the prominent community-driven projects to grow the ecosystem. C# is no longer “vendor lock-in”, but rather an open platform itself.

So C# for Fun?

Six years ago, I would never use C# in a fun personal project. But things have changed. Since now most of my “fun” projects are game related, I actually use C# a lot:

For Unity, C# is pretty much the only choice.

For a game server, C# is quite good if the game client code is in C#. Even if you don’t need to share code between client and server, avoiding language context switch is a big win itself.

Discuss on HN or Reddit