Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

    > After excel, probably the second best thing Microsoft has ever released. Turns .NET into a reasonable platform.
I feel like C# is quite underrated. Very productive language that's relatively easy to teach to anyone that is already familiar with JS or TS. Used in a variety of contexts from game engines to enterprise backends to desktop apps.

A few fumbles by Microsoft early on stunted its growth, IMO, but it's a really good and relatively easy to learn general purpose language.



> 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”.


Yes, big time fumble.

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.).


What do you mean by "minimal syntax for console and APIs"?


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.

https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/to...

(If there’s other syntax changes, I don’t know them, AFAIK it’s the same C# you write)


> If there’s other syntax changes, I don’t know them

The new syntaxes that come to my mind are:

* File-scoped namespaces

* "global using"

* "using static"


> What do you mean by "minimal syntax

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.


Minimal web APIs are structured a lot like Express (whereas .NET web APIs are more like Nest.js)

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/m...


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.


Probably top level statements and minimal APIs from ASP.Net Core


> 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?


I’m definitely one of these people. Maybe I should give .NET a chance.


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.


Less than 50% of software engineers ever used .NET for anything, so the answer is almost certainly yes?


I encountered this within the last week...


Yep, I literally only learned it was cross platform last year when I noticed it as a package dependency.

Mind you, I’m not a coder, and more just a Linux homelab hobbyist.


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?


There are quite a few knobs there. You can AOT-compile the bytecode, as well, and you can ship the whole thing as a single binary: https://learn.microsoft.com/en-us/dotnet/core/deploying/sing...

Size very much depends on what you actually use in the app, but on the order of tens of megabytes.


Also, one of the major changes in the Framework -> Core (now just .NET) shift was making it legally permissible to redistribute the runtime?

So you can ship .NET code with a .NET runtime all in your installer.

Afaik, Framework was only distributable from Microsoft.


Nope, have always been able to ship the redist installers. Just like VC++. https://www.microsoft.com/en-us/download/details.aspx?id=21 (scroll down to 'Installation Instructions')

After all, not being able to ship the runtime with the software to buyers would have been... Bad. In the early '00s


I think I was getting a few things conflated. Is the following correct?

You can't run .NET Framework applications without a system install of .NET Framework.

You can absolutely run .NET (ex-Core) applications without a system install.


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.


Java has been there for years as well.

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.


> it seems like you ship it with .NET Core as a dependency, kinda like JRE for Java programs?

Pretty much yes. (there are also other ways to bundle it)

It's not even called ".NET Core" any more, it's just ".NET", which for version >= 5.0 effectively is the successor to cross-platform .NET Core.

.NET 9.0 will be out shortly - The annual release is expected November 2024, likely this week.


Imagine how much better the branding would be if they had just called it:

    > dot
    > dot console apps
    > dot web API
    > dot desktop apps
Simple logo design.

CLI commands would be like:

    > dot new console
    > dot build
    > dot run
    > dot publish
(Yes, I know I can alias)

It's not too late, Microsoft!


It is basically the same as any cross platform language now. They even recommend Docker containers for deployment.


JRE for Java isn't a thing since Java 8, the official way is to use jlink to create a custom runtime, make use of jpackage, or AOT compile.

You may still find some JREs, which are created by some distributions that take the stuff out of the JDK to make classical JRE, though.


C# is sort of underrated but gets a ridiculously easy ride for actively encouraging bloat and abstraction-ism.

What should be "do a thing" is now design a thing to do a thing then call the thing-do-er IDoAThing.

Alone this would not be so bad but the problem is that you only have this. Extension methods? Fuck off (but don't take them away).

Roslyn is great though.


Its very much a culture thing and not a language thing. C# is very versatile. Go look at Unity code and its a fairly different style. It has its own quirks and there's no such as "the right way" but it doesn't feel like Spring.

Also, extension methods are great. LINQ is amazing and that's partly from extension methods.


Right, but extension methods are a hack to cover up problems with OOP


It's just sugar to turn functional style into .Syntax(), but because of that the tooling is very nice and things like LINQ can be a fluent API instead of LISP.

You can call it a hack but IMO you get the best of both worlds.


Don't get me started on fluent apis lol


"cover up" feels like the wrong sentiment here.

It's adding optionality and in fact, doesn't in any way break OOP at all; it is syntactic sugar.


That's a function of the team, not the language.


Languages bring along a culture. Sone teams buck the culture of the broader language ecosystem, most do not. IMO, the C# culture is closer to enterprise Java than it is to Typescript, for good or for ill.


More than culture, language is a statement of values.

There's reflexivity and path dependency but the culture is downstream of the values for the most part.


    > C# culture is closer to enterprise Java than it is to Typescript
I mean, this really depends on what you're looking at.

Here's a sample from the MongoDB client: https://github.com/mongodb/node-mongodb-native/blob/main/src...

Here's a sample from Storybook: https://github.com/storybookjs/storybook/blob/next/code/core...

Here's a sample from Prisma: https://github.com/prisma/prisma/blob/main/packages/client/s...

Here's a sample from Cal.com: https://github.com/calcom/cal.com/blob/main/packages/core/Ev...

These are all fairly mainstream repos so it's not a one off to see "enterprise" style coding in TypeScript.

It's rather the specific use case that determines the style of the code that's written. In the examples above, there's no need for the teams to choose JavaScript classes and inheritance to model the logic, yet it likely better fits the programming model of those modules.

Look at Nest.js as well: https://github.com/nestjs/nest/blob/master/packages/core/rou...

The whole codebase of Nest.js looks an awful lot like Spring or ASP.NET (controller syntax) probably because there's a lot of crossover in terms of what APIs need at scale and how to logically organize that logic.

It just so happens that most use cases for C# and Java favor applications at a larger scale. Another key difference being object scope and lifecycles -- something that rarely comes into play in the generally single-threaded Node runtime. This being one of the key reasons why dependency injection is a thing in C# and Java land.


Both. The language can afford that usage through the design decisions.

I kind of think the best languages would be those that make creating layers of spaghetti indirection nearly impossible. In practice, it's usually much more specious. Names like "CreatorImplementer", "FactoryAbstrator", "DeviceAccessory" and a half dozen equally amorphous things calling each other as if Thomas Pynchon was writing software.

I think I've seen it in all languages I've worked on professionally. Maybe not in Perl.


I mean, you can do the same in JavaScript and TypeScript as well as any number of other languages.


Agreed. There's plenty of obtuse indecipherable JavaScript these days.

I keep remembering this code I saw in 2014. It was in ember and used mongodb and some bizarre ember features I've long forgotten to stylize things instead of conventional stylesheets.

I had been doing JavaScript since it was called LiveScript in 1995. I knew it very well. This setup was utterly impenetrable. Charging a button color required I think 6 or so different data stores to be manually updated and placed back in alignment because things were validating and cross checking for no good reason other than being a complicated pile of tangled messes. I had to quit the job.

I know how incredible this sounds. I can prove it. He does everything like this. Here you go https://github.com/davideschiera/flame-ui/tree/master ... I'm not quite sure what these 252 files do but I'm pretty sure it could be done in about 25 lines of code in a single function.

I had worked with him previously on a C# codebase almost 20 years ago now. I remember him being just as insane but somehow the language successfully encapsulated the insanity so I never had to deal with it.

There's this fundamental clash. I think things should work, be maintainable, and have the most minimally complex implementation possible - the Ernest Hemingway approach. Some people prefer James Joyce.


Human behaviour in programming is of course totally independent of language and tools, indeed


It is, in fact, independent. You can create horrible monstrosities in any language, or create great code in any language.


Sadly modern idiomatic C# is a bit of a nightmare these days. It's a heavily abstracted/"indirected" style, where you are basically forced to use a debugger to determine where a method call actually goes. The goal seems to be to make things as emergent as possible, with reflection and IL emit determining code flow as much as possible.

I think that F# goes a long way to prevent this, by nature of being functional.


I’ve written a lot of modern C# and I don’t know what you mean - especially with regards to your comment about F# being different.

Are you taking about the popularity of the mediator pattern (via Mediatr)?


The default C# style has always preferred a large dose of unnecessary runtime flexibility. Sometimes that comes in the form of lots of dynamic dispatch, sometimes it's DI, sometimes it's non-sealed overly large base-class types, and sometimes its some combination thereof - but the net effect is that it's not possible to reason about code by just reading it. Tooling becomes worse and/or less valuable because tooling too cannot reliably trace all possible code paths.

Additionally, Microsoft has an outsized influence on the C# ecosystem. As a result, they wrote quite a few architectural and style guidelines that, naturally, target themselves. It makes sense for a hyper-widely distributed library to consider things like binary compatibility as critical features, but the design rules that follow from that have zero utility for apps, and low utility for most libraries. The C# ecosystem also has an unhealthy veneration for MS provided code. While I'm sure most of the authors are quite competent, there's less experimentation and fewer hidden gems because the ecosystem is tilted towards being followers behind the framework providers path.

As a result, it's much harder to refactor code in that style than code in a less (runtime) flexible style, and it's worse when the libraries heavily used are frameworks that have so many extension points. There's a tradeoff between runtime flexibility and codebase flexibility, and the default .NET C# style heavily leans towards runtime flexibility, which isn't a good tradeoff for most software.


Maybe you're just extremely lucky.

MediatR is part of the problem, yes. Way too much attribute/reflection magic there.

One particularly good example is: attempt to add full support (i.e. query text generation) for PERCENTILE_CONT to EF. That will give a good idea of just how bad the idiomatic C# style can get. Now take the EF style of code/architecture and apply it to an entire product codebase - which is completely a understandable mistake to be made (it's not unreasonable to assume that EF would be a good example of C# code). You know you have a problem when you throw in the towel and have ThingDependencies classes all over the place. The aspnetcore internals don't vary too much from the abomination that is EF.

"Just don't code like that," you might say. The problem with that argument is that pre-existing code exists, people don't code in isolation, and the cargo cult has a ton of momentum behind it.

> your comment about F# being different.

You'll struggle to write that style of code in a functional language. There is such as thing as healthy friction. F# has its own problems, but this is not one of them.


Ugh that sounds like the Spring effect. I really like Java but Spring and IoC/DI makes it very complicated.


What in particular do you find difficult about DI?

My experience is that it's "fire and forget"; never give it much of a thought at all and isn't much different from:

    // some-module.ts
    export const someModule = () => {
    
    }

    // other-module.ts
    import { someModule } from "some-module"
Except that DI allows "importing" at different levels.


One of the reasons I use Java is for type safety. What's the point when your app compiles OK but then doesn't start because Spring can't satisfy all the dependencies or choose which one. I spend hours trying to get the config right. You end up programming via config.


> I feel like C# is quite underrated

That’s fair, I consistently underrate it myself. Last time I worked with C# was a big messy codebase written by mostly junior developers who didn’t understand what entity framework was doing behind the scenes. I came away from that experience with the impression that C# is a terrible language.


There’s an interesting problem with “practical” functional languages that we don’t really know what a crappy enterprise F# / OCaml / Haskell codebase looks like - though maybe Haskell is sufficiently popular in crypto these days that there’s some examples. But generally software in these languages is written by highly motivated developers with a lot of flexibility. OTOH a lot of bad C# and Java is written by developers with bad technical management who treats devs as code monkeys. Further a lot of the “technical enablers” of bad C#/Java are bespoke enterprise frameworks that simply don’t exist for functional languages. But in principle, enterprise Haskell could be quite vulnerable to a “productivity-boosting” language extension that locks older Haskell codebases into horrible typeclass golf.

I say all this as someone who loves F# and would only take a C# job if I needed the money. But certainly a lot of the enterprise antipatterns can be clumsily implemented in F#, while a well-written C# codebase is usually pleasant and productive. In particular F# doesn’t totally escape problems like “the Nuget service locator is overengineered for our needs, but we are on a deadline and an excessive abstraction is better than hard-wiring everything.”


My hypothesis is that F# is somewhat self selecting at senior/lead dev's who would have a lot of choice in the language that they get to choose not the army of "code monkey's" as you call them. Generally this would mean higher code quality on average.


C# these days is GREAT, but F# still has features that C# doesn't like unions and much more powerful (and ergonomic) pattern matching.

And line for line, F# is more effective at getting things done (and i would argue with less bugs)


The c# team is supposedly putting in serious work on Discriminated Unions. I'm really interested to see how it turns out because f# already does a great job with them.


Discriminated unions is probably the last "must have" feature I've been waiting for in C#. Better pattern matching would be nice to have but honestly not as much of a gamechanger.


The C# team for several reasons decided "rock solid" pattern matching was a higher priority than discriminated unions because good discriminated union code needs good pattern matching. In the last few versions of C# I think pattern matching has gotten really good. List Patterns finally exist now, and that influenced the new iterable collection initializers, which are also pretty good now. Last I checked, the C# team was still debating Dictionary/Hash key lookup Patterns and Dictionary/Key-Value initializers, and that's maybe my last big request in pattern matching other than native discriminated unions, of course.


What is also great about C# is that the language server for editor support uses the real compiler. I once set that up with emacs and it is by far the best experience I had in emacs with any language.


> relatively easy to teach to anyone that is already familiar with JS or TS.

What about Java?


In my experience having mentored devs into F# on very large projects its easier to learn F# if coming from a JS, Python, Go, etc background than C# or Java even without previous .NET exposure. Not due to a skill issue, but more of a psychological one.

IMO to be productive in F# code code tends to be simpler - many patterns that dev's valued learning in a OO context become a little obsolete or less needed as a whole. This is my anecdote but it is the C#/Java/etc crowd that tend to have the most negative immediate reaction to F# - they ask "where is X" and often I immediately say "X is bad, you shouldn't need X - think about why". They associate X with some kind of power/productivity gain, but don't realize there is many ways to do things. Kind of follows the paradigm of "frameworks/patterns are missing language features".

If they keep at it then it eventually clicks but there is almost a psychological "unlearning" that needs to be done which at least in the C# camp creates an almost tribal tension between the two camps - its just too unfamiliar. Whereas for example TS dev's seem to find it somewhat familiar in parts; even people only ever done JS previously I've found have become good F# dev's. Could be because the flow more suits the way you would code in a dynamic language while still being strongly typed (e.g. REPL's, type inference, static modules, functions over classes, etc).


Vouched for this dead comment. If this really should be flagged to death, could I get a quick refresher on what parts are boring reading or off-topic? The comment is an anecdote and not data, but this does not seem like a flamey post, or a low effort post, or a strawman post.


Probably automated. If someone creates a new account then immediately posts big comments, lots of comments, or links, the comments end up dead by default and need vouching. The dead spam comments often found at the bottom of threads are why.


C# and TypeScript are remarkably similar owing to having come from the same designer. But even before then, it seems as if the two languages influenced one another.

Check out my small repo here showing how similar they are: https://github.com/CharlieDigital/js-ts-csharp


Many of the ideas trace back to Turbo Pascal, Delphi, and J++.


F# is much easier to teach and learn than C#.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: