You can build single-file executables for .NET 4, and I do, but there's no static linking, so you need to restrict yourself to assemblies that ship with the framework and whatever other prerequisites you're willing to install or assume will be installed.
Two notable caveats:
1. Certain newer C# language features require runtime support that doesn't ship with older versions of .NET, which can include quite a few DLLs.
2. In-process COM components can only be instantiated in .NET runtimes runtimes with the same processor architecture, even if the .NET program itself is processor-independent (which by default will use the runtime matching the native architecture), so you may need to ship multiple executables to match multiple COM dependency versions even if your program is otherwise processor-independent or you only intend to ship it for a single processor architecture.
For example, if you build a processor-independent program that relies on the Microsoft Access DAO library, it will by default work correctly on 32-bit Windows if 32-bit Access is installed, and on 64-bit Windows if 64-bit Access is installed, but there's no way to get it to work on 64-bit Windows with 32-bit Access without shipping a second executable (that can be identical with the exception of the 32BITREQUIRED flag in the CLR header[1] that can be set as part of the build process or with the .NET SDK corflags utility).
Rather than ship separate "32-bit (or 64-bit with 32-bit <bar>" and "64-bit" versions of a program <foo> that interops with some in-proc COM component <bar>, I prefer to build a single executable <foo>.exe, then create a copy <foo>32.exe with the flag set as part of the build process, and ship both to be installed side-by-side.
Then, since the native COM DLL isn't loaded until you actually create an interop object referencing it, you can just try to create an Interop.<Bar>.<something> object at program start, catch ComException with HResult == REGDB_E_CLASSNOTREG, then in the exception handler, run <foo>32.exe as a subprocess with identical command-line arguments and exit with <foo>32's exit code, unless you're already running in a 32-bit CLR, in which case you just fail normally.
[1] https://ecma-international.org/wp-content/uploads/ECMA-335_5...