# Wednesday, December 20, 2006

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.

Wednesday, December 20, 2006 8:40:10 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Tuesday, December 05, 2006

We often talk about the loaded assemblies list over here. This is the list of assemblies currently loaded in the app domain. Big surprise. There isn't anything incredible special about this list. It is pretty easy to access and use. At the minimum, you can print out the list of assemblies. Beyond that, you can load and instantiate any of the types within those assemblies. That's where reflection comes in.

Here's some code that prints out the list of loaded assemblies:

using System;
using System.Reflection;

namespace GetLoadedAssemblies
{
class Program
{
static void Main(string[] args)
{
Assembly[] asms;

asms = AppDomain.CurrentDomain.GetAssemblies();

Console.WriteLine("There are {0} assemblies loaded:", asms.Length);
foreach (Assembly asm in asms)
{
Console.WriteLine(asm.FullName);
}

Console.WriteLine();
Console.WriteLine("The currently executing assembly is:");
Console.WriteLine(Assembly.GetExecutingAssembly().FullName);

}
}
}
Tuesday, December 05, 2006 11:51:00 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Wednesday, June 21, 2006

I've been running an early adopter program for a couple months now for a bunch of changes that we're making to the CLR binder/loader in the CLR v3 timeframe. The changes are related to the binding model, versioning, servicing and an add-in model. Also, just to be clear, this isn't .NET Framework v3.0 (AKA WinFX). This is the one after that. Here is the blurb that I sent folks who I thought might be interested. Please @ mail me if you are interested in participating.

We have been busy designing some exciting new changes to the CLR loader/binder. We'd like to start collecting feedback from customers about our change early, in order to ensure that the final product is rock solid. The first phase of the program is to validate that our changes are useful and to understand the compatibility of these changes. In order to collect this information, we’d like you to run two tools on your system so that we can learn more about how you – actually the managed apps you use – use the loader and GAC. The one tool is a tool that collects very basic information about the GAC, while the second tool is actually an instrumented CLR which logs data about how the loader, binder and domains are used. The next phases involve running a prototype version of the CLR that includes the new changes, but we’ll get to that later, as it comes available.

There are also a couple legal documents to sign, specifically an MSFT NDA and a license. I'm happy to discuss those if you want to go to that stage.

Wednesday, June 21, 2006 1:38:12 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
# Tuesday, August 16, 2005

I was recently reading the "remarks" section for the AppDomain.Unload() method @ http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemAppDomainClassUnloadTopic.asp. I was doing a review of the documenation available to determine if we needed more around the CannotUnloadAppDomainException exception. I decided that we were in an OK spot for the most part, but that I would post a little more information on this exception for the small percentage of .NET programmers out there who need it. At least, I certainly hope that most .NET programmers don't need this information ;)

Note: Please mail me if this information proves particularly useful for you -- I'd like to better understand your scenario.

Upon calling the AppDomain.Unload() method, the CLR will request that all threads in the unloading domain abort. Threads do not abort instantly, so getting n threads to abort will take some time. As a result, the CLR checks after 10ms* to determine if there are any threads still running. If there are not any threads still running, then the CLR will proceed with domain shutdown. If there are threads still running, then the CLR repeats this process -- wait 10ms and check -- up to 1000* times. In the general case, it is very unlikely to need to repeat this process 1000 times (or anything close to it); eventually, all threads will be aborted and domain shutdown will occur. The purpose of this repeating "wait and see" behaviour is to avoid a deadlock due to a thread that will not abort for some reason.

If the 1000th check fails, meaning that threads are still running (not yet aborted), the CLR will throw a "CannotUnloadAppDomainException" exception. If the application, after the exception has been thrown, can do something to prevent this situation from occuring again (i.e. cleanup or manually aborting threads) then the application should perform that cleanup and call AppDomain.Unload() again. It is not expected, however, that the app should attempt to try repeat this process -- catching the "CannotUnloadAppdomainException" exception and calling AppDomain.Unload() -- an indeterminant number of times until it "works". The best approach is to remove the case where the exception is thrown at all, which means ensuring that all threads in a domain can be aborted in a timely manner. If the application, after the exception has been thrown, cannot do anything to prevent this situation from occuring again, it should not call AppDomain.Unload() again, but terminate the application or leak the domain.

* please consider these numbers (10ms and 1000 times) as implementation details. You absolutely should not take a dependency on them, for two reasons: (A) they are subject to change in a later release w/o notice, and (B) the number of running threads, processes and CPUs (+ multi-core) can very significantly change the time it takes to hit the 1000th iteration of the check due to the amount of CPU time that the unload check is given.

Tuesday, August 16, 2005 3:03:38 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [1]  | 
# Wednesday, December 15, 2004

I was reading the CLR newsgroup and noticed the following post about unhandled exceptions. I wasn't quite sure exactly how this worked, so I wrote a quick app that caught exceptions from other domains in the defaul domain. This behaviour was what the poster was looking for.

You can download the code [Everett | Whidbey ] to see how it works.

The basic idea is that you register for the AppDomain.UnhandledException event. This event will be fired for any exception that is not handled in any domain in the process. You cannot swallow the exception at this point, so your app will die no matter what. You can, however, do any cleanup that is required or put up some kind of user notification. In the case below, you can see that I tell the user that an unhandled exception occured. I'm sure that this is going to be quite useful to them ;)

Here is the bulk of the code:

    class Program
    {
        static void Main(string[] args)
        {

            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

            AppDomain domain2 = AppDomain.CreateDomain("domain2");
            domain2.CreateInstance("DomainLib", "DomainLib.ThisClassThrows");

        }

        static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;
            Console.WriteLine("Unhandled exception!!");
            Console.WriteLine(ex.InnerException.Message);
        }
    }

Another option, which is a good idea, is to use try/catch blocks in your code around calls to other domains. That way you can catch the exceptions that come across the domain boundary as opposed to just settle for process termination.

Wednesday, December 15, 2004 6:45:03 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [11]  |