Assemblies aren't marked as domain-neutral in any specific way. You don't have to give them some specific attribute to make them domain-neutral. Any assembly can be loaded by the CLR either into the shared domain or the domain that triggered the assembly load depending on the configuration of the CLR instance that is loading the assembly.
How the CLR instance decides to load an assembly is dictated by policy. There are several ways to explicitly set this policy:
An assembly loaded as domain-neutral will be loaded into the shared domain. The app domain name is "EE Shared Assembly Repository" in CLRv4. That's not a real app domain, because it has no data and can't run any code. Assemblies loaded into it will share its code among all other running app domains. The byte code in the assembly will be JIT-compiled only once. All mutable data in the assembly, however, will be duplicated among the running domains. Static fields are not shared between app domains. Per-app domain static fields will be duplicated and different app domains will read and write in different places in the memory when referring to the same static field.
Aside: there is another kind of static fields - RVA statics, that are shared among all app domains in the current process. There is no way to declare such a field in C#, but it can be done in C++/CLI.
There is a trade-off in using domain-neutral assemblies. Access to static fields is slower. Since they're JIT-ted only once, but may access multiple instances of a per-app domain static field, any access to a static field goes through an additional indirection. When an assembly is loaded straight into the running domain, the address of the static field can be directly embedded into the JIT-ted code. However, when code compiled into the shared assembly tries to access a static field, it must first load the current domain's context and then find in it the static field address for this domain.
The decision whether to load an assembly into the shared domain or into the running domain depends on your use case, more specifically how many app domains you'd create and what sort of core you'd load into it.
- If you load multiple domains that run essentially the same code, you'd want to share assemblies as much as possible, unless it's significantly hurting the performance of accessing static fields. An example is an application that decides to run portions of its own code in a separate app domain for the sake of isolation.
- If you load multiple domains with different code, you'd want to share only assemblies that are likely commonly used by all the different assemblies. These would usually be the .NET Framework's own assemblies and all assemblies loaded from GAC. IIS works this way by default when running ASP.NET apps.
- If you ever use only one app domain, you shouldn't share anything. A regular GUI application will be like that.
Note: mscorlib is always loaded into the shared domain.
Sources and further reading: