8 ms·
Having worked with C# professionally for a decade, going through the changes with LINQ, async/await, Roslyn, and the rise of .NET Core, to .NET Core becoming .N
by devjab 1y ago
Having worked with C# professionally for a decade, going through the changes with LINQ, async/await, Roslyn, and the rise of .NET Core, to .NET Core becoming .NET, I disagree. I certainly think that C# is a great tool and that it’s the best it has ever been. It’s also relies on very implicit behaviour, it is build upon OOP design principles and a bunch of “needless” abstraction. Things I personally have come to view as anti-patterns over the years. This isn’t because I specifically dislike C#, you could find me saying something similar about Java.
I suspect that the hidden indirection and runtime magic, may be part of why you love the language. In my experience, however, it leads to poor observability, opaque control flow, and difficult debugging sessions in every organisation and company I’ve ever worked for. It’s fair to argue that this is because the people working with C# are bad at software engineering. Similar to how Uncle Bob will always be correct when he calls teams out for getting his principles wrong. To me that means the language itself has a poor design fit for software development in 2025. Which is probably why we see more and more Go adoption, due to its explicit philosophies. Though to be fair, Python seems to be “winning” as far as adoption goes in the cross platform GC language space. Having worked with Django-Ninja I can certainly see why. It’s so productive, and with stuff like Pyrefly, UV and Ruff it’s very easy to make it a YAGNI experience with decent type control.
I am happy you enjoy C# though, and it’s great to see that it is evolving. If they did more to enhance the developer experience so that people were less inclined to do bad engineering on a thursday afternoon after a long day of useless meetings. Then I would probably agree with you. I'm not sure any of the changes going toward .NET 10 are going in that direction though.
- OtomotO 1y agoI am paid to work in Java and C# among Go, Rust, Kotlin, Scala and I wholeheartedly agree. I hate the implicitness of Spring Boot, Quarkus etc. as much as the one in C# projects. All these magic annotations that save you a few lines of code until they don't, because you get runtime errors due to incompatible annotations. And then it takes digging through pages of docs or even reporting bugs on repos instead of just fixing a few explicit lines. Explicitness and Verbosity are orthogonal concepts mostly!
- npodbielski 1y agoWhat are those magic annotations you are talking about? Attributes? Not much of those are left in modern .net.
- debugnik 1y agoAttributes and reflection are still used in C# for source generators, JSON serialization, ASP.NET routing, dependency injection... The amount of code that can fail at runtime because of reflection has probably increased in modern C#. (Not from C# source generators of course, but those only made interop even worse for F#-ers).
- mrsmrtss 1y agoDon't we have automated tests for catching this kind of things or is everyone only YOLOing in nowadays? Serialization, routing, etc can fail at runtime regardless of using or not using attributes or reflection.
- osigurdson 1y agoEase of comprehension is more important than tests for preventing bugs. A highly testable DI nightmare will have more bugs than a simple system that people can understand just by looking at it.
- mrsmrtss 1y agoI haven't experienced a DI 'nightmare' myself yet, but then again, we have integration tests to cover for that.
- CharlieDigital 1y agoTry Nest.js and you'll know true DI "nightmares".
- 1y ago
- CharlieDigital 1y agoYou are missing the forest for the trees. C# has increasingly become more terse (e.g. switch expressions, collection initializers, object initializers, etc) and, IMO, is a good balance between OOP and functional[0]. Functions are first class objects in C# and teams can write functional style C# if they want. But I suspect that this doesn't scale well in human terms as we've encountered a LOT of trouble trying to get TypeScript devs to adopt more functional programming techniques. We've found that many devs like to use functional code (e.g. Linq, `.filter()`, `.map()`), but dislike writing functional code because most devs are not wired this way and do not have any training in how to write functional code and understanding monads. Asking these devs to use a monad has been like asking kids to eat their carrots. Across much of our backend TS codebase, there are very, very few cases where developers accept a function as input or return a function as output (almost all of it written by 1 dev out of a team of ~20). > ...it is build upon OOP design principles and a bunch of “needless” abstraction Having been working with Nest.js for a while, it's clear to me that most of these abstractions are not "needless" but actually "necessary" to manage complexity of apps beyond a certain scale and the reasons are less technical and more about scaling teams and communicating concepts. Anyone that looks at Nest.js will immediately see the similarities to Spring Boot or .NET Web APIs because it fills the same niche. Whether you call a `*Factory` a "factory" or something else, the core concept of what the thing does still exists whether you're writing C#, Java, Go, or JS: you need a thing that creates instances of things. You can say "I never use a factory in Go", but if you have a function that creates other things or other functions, that's a factory...you're just not using the nomenclature. Good for you? Or maybe you misunderstand why there is standard nomenclature of common patterns in the first place and are associating these patterns with OOP when in reality, they are almost universal and are rather human language abstractions for programming patterns. [0] https://medium.com/itnext/getting-functional-with-c-6c74bf279616 https://medium.com/itnext/getting-functional-with-c-6c74bf27...
- deleted 1y ago[deleted]
- devjab 1y agoI notice that none of the examples in your blog entry on functional C# deals with error handling. I know that is not the point of your article, but that is actually one of my key issues with C# and its reliance on implicit, because like so many other parts of C# you'd probably hand it over to an exeception handler. I'd much rather prefer you to deal with it explicitly right where it happens, and I would prefer if you were actually forced to do it for examples like yours. This is because implicit error handling is hard. I have no doubt you do it well, but it is frankly rare to meet a C# developer who has as much of an understanding on the language that you clearly have. I think this is an excellent blog post by the way. My issues with C# (and this applies to a lot of other GC languages) is that most developers would learn a lot from your article. Because none of it is an intuitive part of the language philosophy. I don't think you should never use OOP or abstractions. I don't think there is a golden rule for when you should use either. I do think you need to understand why you are doing it though, and C# sort of makes people go to abstractions first, not last in my experience. I don't think these changes to the GC is going to help people who write C# without understanding C#, which is frankly most C# developers around here. Because Go is opinionated and explicit it's simply an issue I have to deal with less in Go teams. It's not an issue I have to deal with less in Python teams, but then, everyone who loves Python knows it sucks.
- ed_elliott_asc 1y agoWhat sort of issues do you get debugging? My experience of .NET even from version 1 is that it has the best debugging experience of any modern language, from the visual studio debugger to sos.dll debugging crash dumps.
- feoren 1y agoI have been coding in C# for 16 years and I have no idea what you mean by "hidden indirection and runtime magic". Maybe it's just invisible to me at this point, but GC is literally the only "invisible magic" I can think of that's core to the language. And I agree that college-level OOP principles are an anti-pattern; stop doing them. C# does not force you to do that at all, except very lightly in some frameworks where you extend a Controller class if you have to (annoying but avoidable). Other than that, I have not used class inheritance a single time in years, and 98% of my classes and structs are immutable. Just don't write bad code; the language doesn't force you to su all.
- lock1 1y agoHidden indirection & runtime magic almost always refer to DI frameworks. Reflection is what makes DI feel like "magic". Type signatures don't mean much in reflection-heavy codes. Newcomers won't know many DI framework implicit behaviors & conventions until either they shoot themself in their foot or get RTFM'd. My pet theory is this kind of "magic" is what makes some people like Golang, which favors explicit wiring over implicit DI framework magic. > Just don't write bad code Reminds me with C advices: "Just don't write memory leaks & UAF!".
- feoren 1y ago"Just don't write bad code" means that you can easily avoid some of the anti-patterns that people list as weaknesses of C#. Yes, maybe you inherit code where people do those things, but how much of that is because of C#, and how much is due to it being popular? Any popular language is going to have bucketloads of bad code written in it. Alternatively: "you can write bad code in any language". I'm far more interested in languages that help you write great code than those that prevent you from writing bad code. (Note that I view static typing in the "help you write great code" category -- I am distinguishing "bad code" from "incorrect code" here.) Yes, some programming languages have more landmines and footguns than others (looking at you, JS), and language designers should strive to avoid those as much as possible. But I actually think that C# does avoid those. That is: most of what people complain about are language features that are genuinely important and useful in a narrow scope, but are abused / applied too broadly. It would be impossible to design a language that knows whether you're using Reflection appropriately or not; the question is whether their inclusion of Reflection at all improves the language (it does). C# chose to be a general-purpose, multi-paradigmatic language, and I think they met that goal with flying colors. > Newcomers won't know many DI framework implicit behaviors & conventions until either they shoot themself in their foot or get RTFM'd The question is: does the DI framework reduce the overall complexity or not? Good DI frameworks are built on a very small number of (yes, "magic") conventions that are easy to learn. That being said, bad DI frameworks abound. And can you imagine any other industry where having to read a few pages of documentation before you understood how to do engineering was looked upon with such derision? WTF is wrong with newcomers having to read a few pages of documentation!?
- mexicocitinluez 1y ago> Similar to how Uncle Bob will always be correct when he calls teams out for getting his principles wrong. Is this sarcasm?
- Hendrikto 1y agoI think this is fair criticism. OOP advocates like Uncle Bob always try to sell you 100 often contradictory and ill-defined rules and guidelines for how to “use it right”. Stuff like * objects should model a single concept, or * every domain concept should be an object. These two alone are already contradictory. And what do they even mean? Concretely? Then, when OOP invariably breaks down, they can always point to any of the 100 rules that you supposedly violated, and blame the failure on that. “Yes, it did not work out because you did not do it right.” It’s the true scotsman fallacy. It’s like communism. It would work out if somebody just finally did it properly. Maybe a system that requires 100 hard to follow rules to have even a chance at success just isn’t a great one.