You have to jump through some hoops but it's really not that convoluted and miles easier than good C++.
I wish there was an attribute in C# that was "[MustNotAllocate]" which files the compilation on known allocations such as these. It's otherwise very easy to accidentally introduce some tiny allocation into a hot loop, and it only manifests as a tiny pause after 20 minutes of runtime.
That being said, .NET includes lots of performance-focused analyzers, directing you to faster and less-allocatey equivalents. There surely also is one on NuGet that could flag foreach over a class-based enumerator (or LINQ usage on a collection that can be foreach-ed allocation-free). If not, it's very easy to write and you get compiler and IDE warnings about the things you care about.
At work we use C# a lot and adding custom analyzers ensuring code patterns we prefer or require has been one of the best things we did this year, as everyone on the team requires a bit less institutional knowledge and just gets warnings when they do something wrong, perhaps even with a code fix to automatically fix the issue.
Even when allocations happen, .NET is much more tolerant to allocation traffic than, for example, Go. You can absolutely live with a few allocations here and there. If all you have are small transient allocations - it means that live object count will be very low, and all such allocations will die in Gen 0. In scenarios like these, it is uncommon to see infrequent sub-500us GC pauses.
Last but not least, .NET is continuously being improved - pretty much all standard library methods already allocate only what's necessary (which can mean nothing at all), and with each release everything that has room for optimization gets optimized further. .NET 9 comes with object stack allocation / escape analysis enabled by default, and .NET 10 will improve this further. Even without this, LINQ for example is well-behaved and can be used far more liberally than in the past.
It might sound surprising to many here but among all GC-based platforms, .NET gives you the most tools to manage the memory and control allocations. There is a learning curve to this, but you will find yourself fighting them much more rarely in performance-critical code than in alternatives.