Domain Neutral Assemblies

Chris Brumme’s paper on AppDomains goes into great detail about the design of AppDomain. One of the things he discussed is assembly domain neutrality.


Conceptually, a domain neutral assembly is an assembly that lives across multiple appdomains. Domain neutral assemblies will be jitted only once. The jitted code, as well as various runtime data structures like MethodTables, MethodDescs, will be shared across appdomains. This is great working set perf gain in multi appdomains scenario.


Static fields are scoped by appdomain, and will be duplicated on each appdomain. Class constructors must run in each appdomain, to ensure that those static fields are properly initialized. Due to this constraint, access to static fields in domain neutral assemblies has to go through an indirect layer. Thus static field access will be slower in domain neutral assemblies than in domain bound (the converse of domain neutral) assemblies.


Domain neutral assemblies are stored in a shared area (so called SharedDomain). SharedDomain is simply a repository for domain neutral assemblies. There is no code execution in SharedDomain. Code in domain neutral assemblies is executed in user appdomains.


Today domain neutral assemblies cannot be unloaded, even though all the appdomains using those assemblies have been unloaded.


In v1.0 and v1.1, domain neutral assemblies cannot use NGEN images. In v2.0, this limit is removed. Domain neutral assemblies will be able to use a single NGEN image repeatedly for each appdomain. In the non-domain neutral case, the first appdomain can use the NGEN image. The other appdomains will jit.


Domain neutral assemblies cannot directly access domain bound assemblies. Thus all the assemblies in the transitive binding closure of a domain neutral assembly must all be domain neutral. This constraint is enforced automatically by CLR.


Ideally the decision of making assembly domain neutral should be totally internal to CLR. Unfortunately today’s world is not the ideal world. In v1.0, and in v2.0, the host will tell CLR a hint what assemblies can be domain neutral. And CLR will share those assemblies as much as possible.


There are two ways for a host to specify this hint. One way is through hosting API CorBindToRuntimeEx . Another way is decorating your Main method with LoaderOptimizationAttribute .


CLR provides three kinds of hints for a host:


1. SingleDomain – No assemblies (except mscorlib) are domain neutral. This is the default. Mscorlib is always domain neutral.


2. MultiDomain – All assemblies are domain neutral.


3. MultiDomainHost – All strongly named assemblies are domain neutral. This is what ASP.NET is using.


Hint 3 has an unfortunate consequence that you can never update strongly named assemblies once it is loaded. This is why ASP.Net recommends against putting strongly named assemblies into your bin directory. In v2.0, the meaning of MultiDomainHost changes to the following:


An assembly can be domain neutral if and only if it is in GAC, and all the assemblies in its transitive binding closure are all in GAC.


This change allows people to update strongly named assemblies in bin directory, while keeps the performance requirement of ASP.Net, since all the core infrastructure of ASP.Net does satisfy the requirement above.


Those hints are called “hint”, because they only tell CLR what assemblies can be domain neutral, but they do not dictate that those assemblies must be shared across appdomains. Remember all the jitted code and runtime data structures are shared between appdomains. This can only be done if the same native code is valid for all the appdomains. This is not always true. Specifically, the two appdomains may have different binding policies and security polices, as well as other differences.


Binding Policies


If the binding policies affect assemblies in the transitive binding closure differently in the two appdomain, the two appdomains will load different assemblies. In this case the assembly can not be shared between these two appdomains. To prevent this, CLR does an eager transitive binding closure evaluation in both appdomains, and compares the two binding closures. If the comparison shows two identical closures, the assembly is able to be shared between the two appdomains. If the comparison shows different closures, CLR will not share the assembly. Instead, CLR will make two domain neutral copies of the assembly available in shared domain. Future appdomains will automatically select one of them to share, depending on which is matched (or adding another copy if none of them is matched).


You can see this eager transitive binding closure evaluation can cause quite a bit perf hit. In v2.0 CLR does some extra optimization in binding closure evaluation.


The discussion above is based on the assumption that all the assemblies in the transitive binding closure of the assembly can be found. What if some of the assemblies in the transitive binding closures cannot be found?


One option is to say, “Too bad.” All the assemblies in the binding closure have to be available, or we will not share the assembly. Unfortunately people do ship assemblies without making all the assemblies in the binding closure available. This option is considered as not acceptable.


In the end we settle on the second option: The assembly can be shared, as long as all the available assemblies in the binding closure are the same in the two appdomains, and the missing assemblies in the binding closure are also the same.


Now you see why we have to cache binding failures. Say we did not enforce this. We compare the binding closures on the two appdomains and we say they look good. So we share the assembly between the two appdomains. Now you decide to do some trick to bring the missing assemblies back in one of the two appdomains. Boom! Now we are sharing the wrong code. And it is too late to un-share the code.


Of course one thing we can do is when we detect this happens, we say this is forbidden and we unload the offending appdomain. But this is very confusing. The unloading may or may not happen depending on if the assembly is in the binding closure of one of the domain neutral assemblies. It is hard to diagnose, and hard to explain to people.


So in the end, we decide to cache binding failures. This solves the domain neutral sharing problem, and ensures a consistent behavior.


Security Policies


If the assembly has different security grant sets in the two appdomains, the assembly is not shared. But even if the assembly has the same security grant set in the two appdomains, if other assemblies in the binding closure have different grants set in the two appdomains, this assembly still cannot correctly be shared.


Today only the top level assembly’s grant set is checked. If later CLR sees an incompatible security grant set for any assemblies in the binding closure, CLR will not be able to recover from the error and will fail to load the assembly in the new appdomain. Essentially the host is responsible for not violating this constraint.


So when it is best to use domain neutral assemblies?


  1. There are multiple domains, and
  2. There is no special binding policy, and
  3. There is no special appdomain security policy, and
  4. Unloading is not a concern

In the cases above, CLR can share the domain neutral assemblies as much as possible between all the appdomains.