> A few fumbles by Microsoft early on stunted its growth
Microsoft really, _really_ missed out on a huge opportunity to rebrand .NET into something else when they did the Framework -> Core transition. Despite .NET being cross platform for over ten years and winning benchmarks left and right people still see it a the “slow enterprisey framework that only runs on Windows”.
I've met a handful of folks who used it in the .NET Framework days and hadn't kept up with the changes in Core (multi-platform, object-functional hybrid, minimal syntax for console and APIs, etc.).
You no longer need imports, or namespace, class and main method boilerplate to write a basic C# program; there’s now a way to simply write some lines of code in a file with some implicit imports, and compile/run them like it was more of a scripting language.
It's a beginner friendly and "demo mode" set of features to reduce boilerplate for small programs, such that the minimal "hello world" program is literally 1 line of code.
And a working minimal ASP web app with a http endpoint is not much longer.
C# and .NET was from day 1 designed for "programming in the large" so that large codebases can be sensibly organised.
This however causes some overhead, such that a person coming from Python might have looked at the standard C# "hello world" example with using statements, namespace, class and method wrapping the 1-line payload, and conclude that the language is impossibly clunky and cumbersome. My opinion is the opposite; managing e.g. 50K lines of code without those ways of organising code, is going to be impossibly clunky and cumbersome.
However, it is also true that demo mode is great for 1-file demos that get right to the point.
I think the using statements are fine, even for one-file demos. Python, Go and Node don't have implicit imports like C# and it's fine. I'm actually not entirely sold on the implicitness. Forces the reader to look at the .csproj file and the target framework documentation what is implicitly imported.
Most of the verbosity comes from classes and namespaces. Go and Rust have shown it's possible to design a language for "programming in the large" without classes for everything and with less verbose namespaces.
But to be fair, I'm just getting started with C# so my comment above is likely wrong and biased. Happy to be proven wrong :)
C# has several constructs like anonymous types, tuples, named tuples, and records (for structs and classes). Each has different use cases (and sometimes limited scopes like anonymous types) that can serve different contexts for data modelling.
> with less verbose namespaces
This is, once again, the function of the team and not the language. There is no hard mandate on how namespaces are selected so verbose namespaces is a result of teams preferring it over more concise naming. Part of that might be purely practical. Whereas in JS, you would import a local module by path, C# imports via namespace and convention is to use pathing, but C# will of course allow any namespace convention you like for local modules and does not constrain to strict pathing.
> Most of the verbosity comes from classes and namespaces. Go and Rust have shown it's possible to design a language for "programming in the large" without classes for everything and with less verbose namespaces.
The statement that "C# and .NET was from day 1 designed for programming in the large" is true and factual, supported by design documents that pre-date .NET 1.0, i.e. in the late 1990s
The statement that more-lightweight ways have since been developed of approaching the issue is more subjective. But IMHO it is also largely true, as the programming language world has moved on since then. The C# design has been extended a lot, but is constrained by being backwards-compatible. In some ways it is of its time.
e.g. We might not be "entirely sold on implicit usings" but it was a way to get from existing syntax, to a 1-liner
"hello world". I think that global usings are fine, when used very sparingly. e.g. I am happy for a test project to have a global using Xunit because it will be used so widely in the project code. But not many more global usings.
I very much agree with your comment. C# managed to evolve and adopt many “modern” features while maintaining backward compatibility, which is quite impressive.
Having recently joined a company using C#, and with a background using Go, Python, Node, and similar, I was worried about heavy "enterprise-style" APIs. I was happy to discover the new minimal web API while reading .NET Core documentation.
> Despite .NET being cross platform for over ten years and winning benchmarks left and right people still see it a the “slow enterprisey framework that only runs on Windows”.
Do these people I've never heard of outnumber the people who did upgrade but wouldn't have if it had been branded as a separate product?
It's quite nice now. Tooling is way nicer than it was 10 years ago. My team is all Apple silicon Macs and we deploy to AWS Arm64 instances. Unit tests in GH run on Linux workers.
I was going to ask what the multiplatform deployment story was like, it seems like you've partially answered it - it seems like you ship it with .NET Core as a dependency, kinda like JRE for Java programs?
How heavy of a dependency is it?
And is there a dependency hell situation, if different programs require different versions of .NET Core?
The state of .NET Framework is that it's packaged with Windows so technically it did not require an install - an OS shipped with it.
The state of .NET is something that Java wants to reach in a few years or so. There are basically three ways to deliver an application to the users:
- A single or multiple files containing .NET assemblies and a thin binary launcher (single-file mode controls the exact shipping of this) - this is similar to OpenJDK which requires a runtime to be installed on the system (except OpenJDK does not have the capability to put all assemblies into a single file with a small native section for the launcher).
- A single or multiple files containing .NET assemblies and runtime itself
- A single native executable compiled with NativeAOT
The first two options allow to "merge" assemblies into a single file or ship them separately.
Shipping just .NET assemblies without runtime takes the least amount of space but requires a runtime dependency (it's easy to install on practically any system).
Shipping assemblies + runtime can also be done as a single file where there's a sandwich of native code and .NET assemblies. In order for such executable to not take disproportionate amount of space (65-120+ MiB) it can be trimmed which is recommended - this reduces base binary size down to 10 MiB or less nowadays (and grows as you add dependencies).
And building with NativeAOT relies on the same linker-based trimming but produces a single native statically linked executable. This results in the smallest runtime-independent binaries (they start at about 1-1.2MiB) with the best startup latency but comes with a different performance profile when compared to JIT and is not compatible with features that either rely on JIT or on unbound un-analyzable reflection where ILLink cannot determine the exact scope of code that has to be compiled. The ecosystem has significantly improved since the introduction of this target in .NET 7 to provide users with tools to make their code compatible, if any changes are required at all.
AOT compilation has existed for about 20 years, it only happened to be a commercial product, Excelsior JET, Aonix, PTC, Aicas, are some examples.
What GraalVM, OpenJ9 bring to the table is free beer AOT compilers, and in GraalVM's case, a LLVM like compiler framework that doesn't exist at all in .NET land.
They also have the advantage of using agents to gather the required reflection data, instead of forcing an ecosystem split of having to rewrite existing libraries to make them AOT ready.
There was the Phoenix project from Microsoft Research, which had the goal to replace Visual Studio infrastructure with a CLI based compiler toolchain, but unfortunely that project failed to gain traction within Microsoft.
Shipping runtimes, which .NET Core introduced for .NET, has been a thing in Java land since they exist.
There is also the ability to create single executables, coupled with jlink and jpackage.
Besides OpenJDK offerings, there is a richness of JVM implementations, each to those having other capabilities, like OpenJ9 and Azul with their distributed JIT compiler for example.
.NET is great and I prefer .NET to Java consulting projects when given the option, however it is no accident that Microsoft has decided to become again a Java vendor as well.
For .NET Framework, you are indeed correct, but in practice this was treated largely the same way as e.g. C++ runtime libraries - you just bundled the .NET runtime with your installer.
This has been moot for a long time now, though, since Windows has shipped with some version of .NET preinstalled since Vista.
Microsoft really, _really_ missed out on a huge opportunity to rebrand .NET into something else when they did the Framework -> Core transition. Despite .NET being cross platform for over ten years and winning benchmarks left and right people still see it a the “slow enterprisey framework that only runs on Windows”.