Learning C# and .NET after two decades of programming
kerrick.blog
kerrick.blog
(Funny I am doing a hackathon this weekend where we are writing a game with Unity and I'm using Visual Studio for the first time in 15 years! It's pretty amazing how big a role C# plays in game development as it is used in many other frameworks like Godot)
As for game engines that support C#/.NET/Mono, there's actually quite a few:
Unity https://docs.unity3d.com/6000.0/Documentation/Manual/scripti...
Godot https://docs.godotengine.org/en/stable/tutorials/scripting/c...
Stride (formerly Xenko) https://doc.stride3d.net/latest/en/manual/scripts/index.html (even the new physics engine they're integrating is written in C#, bepuphysics2, it's really cool how deep into the engine code you can dig, it's like 90% C#)
MonoGame https://docs.monogame.net/articles/getting_started/3_underst...
Flax https://docs.flaxengine.com/manual/scripting/index.html
And a few others (like NeoAxis, that one is really niche, but I think their voxel LOD idea was cool, even if it didn't run well for me). I wonder why Java doesn't see the same level of attention, since the runtime itself is pretty good and the modern versions of the language are reasonably nice, yet for Java it seems that there's only LWJGL and jMonkeyEngine.
That said, .NET is really good even when it comes to webdev, I have to admit that ASP.NET feels way more coherent than working with the likes of Spring Boot in Java sometimes does, I guess I'd say that it's more of a focused and less fragmented experience.
I actually used to really like Visual Studio, but around Visual Studio 2015 it felt like very subsequent release was more bloated and slower, and I eventually jumped ship for Rider. But by that point, I was really only using C# for Unity projects, and the rest of the time I was using JS/TS and Python.
If you're building a Blazor app, I'd suggest sticking with its SSR project type. WASM is fine but SSR is far more productive. The hybrid mode is a recent addition, but it has too many rough edges and I'd avoid it.
It’s been the second time in my career I’ve been surprised by not hating C# (the first was goofing off with Unity in 2018). The language itself has a lot of niceties; for example a method to turn the variable foo into the string “foo”. The Neovim LSP set itself up just by installing the dotnet executable. And the syntax for creating complex workflows were pretty ergonomic once an experienced .NET dev walked me through what I was even looking at. I still prefer FastAPI + well-typed Python as the backend framework of my dreams… but I’d work in .NET again.
Blazor hasn’t sold me yet, but seems like a fine choice. It fits in the same class of tools to me as Django Templates, HTMX, or JS handlebar rendering. There’s a class and size of apps for which that’s perfect, and there’s some value in a fullstack language keeping your stack monolingual. But IMO the framework should stand on its own against frontend frameworks like React, Vue, or Svelte… with the simplicity of monolingualism added as a cherry on top. Otherwise you’re optimizing towards the number of languages your devs need to learn over which frontend framework would be the best fit for your app. And between the DX and expansiveness of the JS ecosystem, it’s been hard to imagine going back once you’ve spent a few years eating the shamefully-complicated-constantly-shifting-and-reforming elephant that is learning TS React and friends
Can I say again how nice it was to use EmberJS from its release candidate days all the way through my seven year tenure at that job? Batteries included, Promises way back in 2013, and way more stable than anything else.
I love C# and wish I could write all my web stuff with it. Unfortunately, Blazor WASM is so far behind pretty much all JS front end frameworks in terms of performance, bloat, and memory usage.
https://krausest.github.io/js-framework-benchmark/current.ht...
export DOTNET_CLI_TELEMETRY_OPTOUT=1
, but believe it or not, I'm on Linux and I use C# every day.The most significant advantage over other languages I noticed is that it is a very versatile language. You can do basically anything with it and it usually works cross platform out of the box. CLI, GUI, WebService, Mobile Apps and even WASM is also included. With the new PublishNativeAot option you can even build native apps without depending on the runtime. It's not Rust or go in size or performance, but it often is close enough. And boy there are great libraries for nearly everything.
Command Line App? - Spectre.Console
WebService or Api? - FastEndpoints
Cross platform GUI? - AvaloniaUI
Image handling? ImageSharp
ProcessHandling? CliWrap
Unit-Testing? xUnit, FluentAssertions and NSubstitue or the new TUnit
Hate Exceptions? Rust-Style error handling is possible [1]
The only thing I'm really missing is a well maintained cross platform audio library with a permissive license. That's why I'm learning P/Invoke now to create a C# wrapper around miniaudio[2]. So I'm still learning C# after all these years and haven't hit the wall.Good luck!
I just think it's good practice to decouple the frontend. If you're a tiny team and you already know how to use something like Blazor, maybe it makes sense. But that's the only place I see it being valid.
Combine that with pattern matching and you can go quite far. In place of macros you can use source generators, although they are both weaker and stronger than proc macro depending on what you use them for.
Its most impressive feature, however, is LiveView, which is akin to server-side rendering. It’s more sophisticated than that. It does a diff on the state changes and only the diffed DOM node states are transmitted over the network, resulting in lightning-fast performance. In other words, if a value changes from <p>5</p> to <p>10</p>, only the 10 gets sent from the server (along with sufficient information to locate the correct location of the replacement).
Teams using Blazor are often not equipped to understand the stack and the intricacies of how it actually works under the hood, and therefore do not monitor for issues. And worse, when they become aware of these issues, they can do nothing to fix them (well, you technically can, most Blazor teams are not those teams).
>It’s essentially the React of the C# world,
No it is not even close to similar.
If you are using Blazor on a website today I bet you a very, very large amount of money your users are consistently running into issues they cannot recover from.
I know how Blazor works. A lot more than I care to.
If you are actively using Blazor today that's a huge fuck you to both your end-users and anyone being forced to interact with the application.
Things will work fine in QA, but it's not a robust framework. So when things break, they break hard, leaving your end-users in the ditch.
Something like Phoenix LiveView is a much better option.
I have never seen a SignalR (and recently Blazor) implementation without significant and characteristic issues the moment you actually star to observe end-user errors/issue/anatomies. Either through automated reporting or user feedback. And this is because SignalR is built on .NET and .NET has many different threading issues when you need those thread to be stable and robust (not talking about transit B2B apps)
It always happens. Without exception.
You do not have to take my word for it, look at the active, open, and historical issues here: https://github.com/SignalR/SignalR/issues
The issues are plainly fundamental.
It's absurd. And it only survives because it's Microsoft.
This is mostly no longer the case in modern .NET. SignalR is a core part of the framework and completely rewritten.
In our case, Blazor Server has been very stable for us, also for multiple users.
In particular, I am a big fan of the Maui blazor hybrid stuff. Worth a look if you fancy making a desktop app.
I've used; React, Blazor and svelte professionally but never Angular. Sure, the node ecosystem has warts but I've found both the DX and end product of Blazor to be so wildly behind the JS ecosystem that I can't see myself choosing it again. Is there something uniquely bad about Angular or something?
This is a bit unfortunate. It doesn't have to be this way - i.e. you don't need to write "extra" syntax many adopt out of blind adherence to old, outdated practices (i.e. 'private' or'internal class' usually makes no sense to use - these are default visibility levels anyway, you're repeating yourself, or reimplementing Java-style heavy visitor patterns).
If you see there's a shorter path to get from point A to point B it's almost always a good idea to take it. Even if something seems less than optimal, it will still run an order of magnitude faster than any interpreted language. There are obvious language-agnostic gotchas you may want to avoid but otherwise there is no reason to not use e.g. LINQ in regular application logic.
This is by design because each indicates catastrophic unrecoverable failure (you could have a couple of hand-wavy arguments about AVE dereferencing wrong address but, really, that indicates critical error in implementation or memory corruption still).
You should never see any of these regardless. Aside from abusing stackalloc by passing a large number I guess.
What do you mean? if you add "throws Exception" to every method then you can defeat checked exceptions but please don't do this in production code
Config config;
try {
config = readConfig("/some/path/to/a/file/that/must/exist.txt")
} catch (IOException ex) {
throw new RuntimeException(ex);
}
doSomeStuffWith(config);
Realistically situations like this should be: Config config = readConfig!!!("/some/path.txt"); // !!! being some operator that says "shut up compiler crash if this happens"
doStuffWith(config);
People do not like checked exceptions because they either need to check and colour the whole stack or write way too much code to panic.Programmers are LAZY. Making them do verbose things when they can't possibly handle it are going to cause them to reject the language feature.
All of the above examples of custom exceptions are great examples of things that should never be handled by exceptions. If the author was using a library that really threw exceptions like this, then it would probably be best to just find a new library which isn't arbitrarily throwing exceptions to create control flow logic.
Real life does not respect encapsulation, that is, it is not really your application's business to know that such a thing as a SQLException exists, other than how to log it, if SQL is hidden beneath a DAO layer. I mean, just because the beautiful architecture of your application doesn't have things like backhoes cutting through fibers in it, doesn't mean those things can't happen to you. There are a lot of things that will happen to your application that it can't understand and the best it can do is: (i) protect its own integrity (use finally) and (ii) abort, retry or ignore and (iii) hopefully have the wisdom to choose the right one of those.
So you prefer to check for errors after every function call? That's what you had in C language, and the problem is that the logic gets buried inside all the error checking, and that makes code hard to read (and write).
https://gen5.info/q/2008/07/31/stop-catching-exceptions/
Checked exceptions don't cause people to write good error handling code, they just cause a crisis for no good reason when you are writing code that people will answer with some lame answer like
try {
action();
} catch(ActionException x) {}
or try {
action();
} catch(ActionException x) {
throw new RuntimeException(x);
}
or try {
action();
} catch(ActionException x) {
throw new SomeExceptionThatWillCauseACrisisLater(x);
}
when you really should be doing something like try {
action();
} finally {
makeItRight();
}
and finally deal with the exception at the end of the "work unit", seehttps://gen5.info/q/2008/08/27/what-do-you-do-when-youve-cau...
Look at the JDK 8 streams library of an example of a library that (a) is awkward as hell because you can't
stream.map(this::someMethodThatMightThrowAndException)
but (b) still handles errors improperly. Some versions of Lisp have a much better approach described herehttps://gigamonkeys.com/book/beyond-exception-handling-condi...
in most languages what you can do is build a framework that controls execution in such a way that (in the context of a stream of work units) it can "separate the code that actually recovers from an error from the code that decides how to recover" as that article says as best you can.
That's not a valid argument. Lots of people believed the earth is flat but we now know that's not true. Did you read the article? Explain why it is wrong.
It didn't, and they aren't bad.
Checked errors are good and are so important to have program correctness, but you need a way to easily escape them when necessary. Rust and Swift have both have realized this. Rust provides ? and Swift has try!, Java needs the same.
> Look at the JDK 8 streams library of an example of a library that (a) is awkward as hell because you can't > stream.map(this::someMethodThatMightThrowAndException)
This is again Java the language not providing the necessary tools. Checked exceptions can work across high order functions: https://docs.scala-lang.org/scala3/reference/experimental/ca...
(1) We were trying to parallelize an easily parallelizable task in Scala. Except it would only use a fraction of the CPUs available and didn't always give the right answer. In three days I was no closer to getting the Scala solution using all cores, I was able to do it in three minutes with Java Executor.
(2) I saw a somewhat larger code base that implemented a data processing pipeline. I was told by the eng manager that (a) we do code reviews and (b) we use monads for error handling. I guess we did, except it was the monad equivalent of
try { something() } catch(SomeException x) {}
most of the time which, once more, fits pattern of people writing exception handling code to silence the compiler.In principle something like monads could let you implement more complex error handling strategies (like Lisp) but so long as "a monad is a like a burrito and a computation is like a graph", monads will be underpowered. Note you can use polymorphism for handling errors in Java too, for instance
interface FunctionThatThrows<In,Out,X> {
Out applies(In arg) throws X;
}
JDK8 streams could have done better with the tools it had.Other languages use union types, enums, or other type system constructs to represent operations which may have multiple possible outcomes. You're correct that these languages don't support business logic exceptions, but that also isn't an acceptable practice outside of Java.
What is the acceptable practice outside of Java?
Do you prefer to check for errors after every function call? That's what you had in C language, and the problem is that the logic gets buried inside all the error checking, and that makes code hard to read (and write).
Pattern matching, nullable types, and the Try* method conventions are also ways of representing the potential for failure explicitly.
A b() throws C
fn b() -> Result<A, C>
A a = try {
b()
} catch (C c) {
new A()
}
val a = b() match {
Ok(a) => a
Failure(c) => A()
}https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals...
I’ll be sure to post an article once I find out. :-)
https://learn.microsoft.com/en-us/dotnet/fsharp/whats-new/fs...
Dotnet has compilers for C#, VB and F# and they can all share the same frameworks and libraries. C# is the most popular.
C# has been receiving many functional features over the years including pattern matching, composable data processing (Linq), tuples, and immutable data types. I'm looking forward to a good implementation of discriminated unions now.
:D
I'm curious, could you link the lawsuits?
My current preference is to lean on string interpolation [0] to do most of the heavy lifting when rendering web documents on the server. You can nest interpolations and use LINQ + collection aggregation (string.Join()) to trivially build things like tables and other complex documents. Combine interpolation with verbatim [1], and you basically get a poor man's PHP document system nested within your C# source files. I've done the whole razor template thing and I am completely over that. Managing the dependencies and weird initialization routines for these rendering engines is not worth it when the language has a built-in feature that accomplishes virtually the same thing with absolutely zero BS.
Writing raw HTML to the client is a simple one-liner [2]. You can quickly build your own bespoke web frameworks, leveraging the power of features like reflection to make your life 100x easier. You can sidestep virtually all of the AspNetCore namespaces and the attribute-driven nonsense that is hallmark of most .NET/MVC-style web apps. If you keep it light and avoid the boilerplate, you can do a lot of damage within 100 lines of code these days.
[0] https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
[1] https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
[2] https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspne...
"Patterns of Enterprise Application Architecture", even just the title of this book reminds me of the good old time when you would hire 30 java consultants to do online forms for business with java beens, j2e, .JSP and co, delivering the worst and most buggy user experience facing a lean website like "facebook" created by 2 students doing simple html/php apps.