I wrote a recent post about getting the list of loaded assemblies. That’s a pretty straight-forward operation as shown by the code listed in that post. I’d like to step up the problem one notch by looking at getting the list of assemblies from another domain. Seems like a very reasonable thing to do ... Or we’ll see …
To properly setup this example, I need to create another domain and keep a reference to that domain. That’s easy:
//Create new domain
AppDomain domain = AppDomain.CreateDomain("domain2");
Next, we need to load some code in that domain, or else there won’t be any assemblies loaded there to actually query. Note that Host is one of my types, as you’ll see later. The method below loads the DomainHost assembly in domain2, creates an instance of the DomainHost.Host type in domain2 and also returns a reference to that instance in the current domain. That’s why you see the cast to Host.
Host host = (Host)domain.CreateInstanceAndUnwrap("DomainHost", "DomainHost.Host");
OK … this is the part that where it gets interesting. It is actually a little non-obvious on how you are intended to get the list of loaded assemblies from the other domains. Let’s first look at the most intuitive approach for doing this. There is the same GetAssemblies() method on AppDomain that I used in the last post. So, given that I have a reference to the domain2 AppDomain instance, why not just call GetAssemblies() on it? This is what the code would look like:
domain.GetAssemblies();
For a variety of reasons, assemblies are not marked as serializable objects, which means that they cannot be remoted across an appdomain (or any other) boundary. You can get around this limitation by remoting and then loading an assembly as a Byte[], but that’s a different blog post. This call gets around the limitation by remoting an array of representative AssemblyName objects, as opposed to the actual assemblies. The loader then does the equivalent of the following, assuming the AssemblyName[]is called asmNames:
foreach (AssemblyName asmName in asmNames)
{
Assembly.Load(asmName);
}
This behavior is not really expected or desirable. First, you really want the AssemblyName objects, not the Assembly objects, and you definitely do not want to cause the assemblies to be loaded in this domain. In addition, if the assemblies are not located in the GAC, and the other domain has a different AppBase, then it is very likely that the assembly loads will fail, resulting in the loader throwing an exception that you might not be prepared to catch. Ouch. That’s really bad. It is also important that not all assemblies play nicely with this behavior. For example, Reflection.Emit assemblies cannot be re-loaded cross domain, for fairly obvious reasons. Mixed-mode assemblies have another set of problems. So, let’s skip that option.
Another option is to call AppDomain.GetAssemblies() in the other domain, and then to remote the Assembly[] back through to the current domain. That will have the same effect as what I just discussed above.
I’ve skipped over some important details that I need to explain. My DomainHost.Host type is a regular old that type that happens to inherit from System.MarshalByRefObject. The fact that it inherits from this type is more of a marking than anything else, as I don’t override any of MBRO’s methods, nor do I have to know what they are. This is very conceptually similar to marking a type with the [Serializable] attribute. The difference is that MBRO provides copy-by-ref semantics as opposed to copy-by-value across a boundary, which is what the [Serializable] attribute provides. This means that MBRO-inherited types can be remoted across a domain, process or even machine boundary as a reference that you can easily call across. The method calls only affect the domain in which the type actually resides (not where the reference resides), with the exception that return types (which must be either MBRO or [Serializable]) are returned to where the call was made from (the current domain). The reference is called a transparent proxy, and you can see that in the debugger when you try to peer into what turns out to be the largely opaque System.Runtime.Remoting.Proxies.__TransparentProxy type. Woah! The fact that DomainHost.Host is MBRO is going to turn out quite useful, as we’ll see.
OK, now to the real solution. I need to add a method to DomainHost.Host that I can call that returns AssemblyName[] and that does not interact with the loader in any way on this side of the domain boundary, as I want to avoid any assembly loads on these AssemblyName objects. Let’s see what the method would look like:
public AssemblyName[] GetAssemblyNames()
{
AssemblyName[] names;
Assembly[] asms;
asms = AppDomain.CurrentDomain.GetAssemblies();
names = new AssemblyName[asms.Length];
for (Int32 i = 0; i < asms.Length; i++ )
{
names[i] = asms[i].GetName();
}
return names;
}
This method uses the AppDomain.GetAssemblies() method to get the list of loaded assemblies, but doesn’t return that. It creates an equal length AssemblyName[] array to the Assembly[] that it already has. It then populates that array with the assembly name of each of the assemblies. After the assembly name array is populated, the method returns that array of serializable objects back to the caller, which happens to be on the other side of the appdomain boundary. And how does one call such a method on the other side of the appdomain boundary? Easy:
AssemblyName[] asmNames = host.GetAssemblyNames();
This ease of cross-boundary calls is the beauty of .NET Remoting. You just call methods as if the instances were located directly beside you. Cool.
Well, that’s now about it. Let’s take a look at the whole program:
Main program:
using System;
using System.Reflection;
using DomainHost;
namespace GetAssemblyNamesFromDomain
{
class Program
{
static void Main(string[] args)
{
AppDomain domain;
Host host;
AssemblyName[] asmNames;
//Create new domain
domain = AppDomain.CreateDomain("domain2");
//Load assembly in domain2 and create instance of DomainHost.Host
//A reference to the instance of DomainHost.Host is returned
host = (Host)domain.CreateInstanceAndUnwrap("DomainHost", "DomainHost.Host");
//Most obvious method for getting the list of loaded assemblies.
//This method will cause the assemblies in the remote domain to
//be loaded in this domain, which frequently won't work. Ouch!
//domain.GetAssemblies();
asmNames = host.GetAssemblyNames();
Console.WriteLine();
Console.WriteLine("Printing assemblies:");
foreach (AssemblyName asmName in asmNames)
{
Console.WriteLine(asmName.FullName);
}
}
}
}
DomainHost.Host (in DomainHost.dll)
using System;
using System.Reflection;
namespace DomainHost
{
public class Host : MarshalByRefObject
{
public Host()
{
Console.WriteLine("Loading host in domain {0}",AppDomain.CurrentDomain.FriendlyName);
}
public AssemblyName[] GetAssemblyNames()
{
AssemblyName[] names;
Assembly[] asms;
asms = AppDomain.CurrentDomain.GetAssemblies();
names = new AssemblyName[asms.Length];
for (Int32 i = 0; i < asms.Length; i++ )
{
names[i] = asms[i].GetName();
}
return names;
}
}
}
The output of the program looks like:
Loading host in domain domain2
Printing assemblies:
mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Microsoft.VisualStudio.HostingProcess.Utilities, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
DomainHost, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
The fact that I see “Microsoft.VisualStudio.HostingProcess.Utilities” loaded is a VS weirdness. VS is loading an assembly in domain2 to somehow “help me”, but I don’t know why. This only occurs when I’m running/debugging my app through VS. If I run the program outside of VS, I don’t see that assembly loaded. You have noticed executables ending in vshost.exe. That’s a similar issue, but for another day.