Most of protection solutions use a classic approach: obfuscation by renaming classes, methods and properties. It is supposed that in case of hacking much more time will be wasted. It sounds reasonable, but the problem is that instructions are the same, and it is so easy to patch it.
Another popular way to protect applications is to use undocumented features of .NET runtime. That could work but, as soon as a new .NET runtime version is released, this method stops working.
The third idea is to use unmanaged CLR debugging interface that injects real .NET code at runtime. Unfortunately, a hacker can get this code after injecting, then remove unmanaged part or replace original code with modified one.
Virtual Machine that makes code completely incomprehensible. The goal is to convert the original code into an illegible form. ArmDot has a proven approach that is widely used for the unmanaged code: the original code is converted into an array of bytes that is interpreted by special virtual machine. Each time you apply ArmDot, it creates a new version of virtual machine and uses a new set of instructions to represent the original code.
It's easy to add dependencies: managed and unmanaged DLLs, data files and other assets to the output .NET assembly.
If a .NET code requires an unmanaged DLL, just add the DLL to Assembly Directory and build a project.
After building the project, the output .NET assembly will work as if the embedded DLL really exists.
In other cases one may need to hide assets: videos, images and other files. Embedded files are not stored on a disk, but in the process memory instead, therefor it's hard to extract them.