AppDomains, APIs, and Identical Static Types Within the Same Process

Processes isolate applications, but when thinking about Application Domains (AppDomains) it helps to take into account not only processes, but assemblies.  An application domain is a logical container that allows mulltiple assemblies to run within a single process.  However, the AppDomain itself is a boundary, a partition within the operating system process, for assemblies and their types.  As processes are isolated from each other in the operating system, so assemblies are isolated from each other within AppDomains.  

Typically, an application without some kind of snap-in API with the responsibility of hosting third party components, will only have one AppDomain.  However, applications that provide libraries which contain classes from which third party developers can inherit, usually host the assemblies those third party developers write in a separate AppDomain, away from the types and logic of their product.  Here are some useful facts about AppDomains. They ...

  • are type boundaries
  • can be used to isolate tasks that may cause a process to abnormally terminate
  • can be loaded and unloaded during runtime, minimizing machine resource utilization issues for conceivably problematic assemblies

Definitions

All definition content has been paraphrased from MSDN.

Evidence: Information such as the strong name of an assembly, the site of origin, application directory, or cryptographic hash that identifies the AppDomain to the CLR.  Evidence resolves to a set of permissions granted by the CLR security policy.

AppDomainSetup: A class which provides AppDomain initialization properties that represent assembly binding information.

MarshalByRefObject: The base class for objects that communicate across application domain boundaries using a proxy.  The first time an application in a remote application domain accesses a MarshalByRefObject, a proxy is passed to the remote application. Subsequent calls on the proxy are marshaled back to the object residing in the local application domain.

Procedure

The example solution, which can be downloaded by clicking the download link below, was created first by selecting the Console Application Project Template.  The structure of the full solution is shown below.  

Solution Structure in Visual Studio 2013

AppDomainLoadUnload Example Solution Structure

 

There are only two files in the solution, Program.cs and SeparateAppDomainAssemblyLoader.cs, but SeparateAppDomainAssemblyLoader.cs contains two classes.  They are described in the image below.

Classes Required for Assembly Loading in a Child AppDomain Object

SeparateAppDomainAssemblyLoader.cs Class Descriptions

 

Here is the code for the two classes found in SeparateAppDomainAssemblyLoader.cs. Note the inheritance from MarshalByRefObject in the inner AssemblyLoader class.  After the child AppDomain instance is created, via the factory method named BuildChildDomain, it is this class the child AppDomain uses to load assemblies into itself.  

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Globalization;
using System.Security.Policy;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;


namespace AppDomainLoadUnloadEx2
{
    /// <summary>
    /// Loads an assembly into a new AppDomain and obtains all the namespaces in the loaded Assembly, which are returned as a 
    /// List. The new AppDomain is then Unloaded.
    /// 
    /// This class creates a new instance of a <c>AssemblyLoader</c> class which does the actual ReflectionOnly loading of the Assembly into
    /// the new AppDomain.
    /// </summary>
    public class SeparateAppDomainAssemblyLoader
    {
        public AppDomain childDomain;

        #region Public Methods
        /// <summary>
        /// Loads an assembly into a new AppDomain and obtains all the namespaces in the loaded Assembly, which are returned as a 
        /// List. The new AppDomain is then Unloaded 
        /// </summary>
        /// <param name="assemblyLocation">The Assembly file 
        /// location</param>
        /// <returns>A list of found namespaces</returns>
        public List<String> LoadAssemblies(List<FileInfo> assemblyLocations, string topLevelNamespace)
        {
            List<String> namespaces = new List<String>();
 
            childDomain = BuildChildDomain(AppDomain.CurrentDomain);
 
            try
            {
                Type loaderType = typeof(AssemblyLoader);

                if (loaderType.Assembly != null)
                {
                    var loader = 
                        (AssemblyLoader)childDomain.
                            CreateInstanceFrom(
                            loaderType.Assembly.Location, 
                            loaderType.FullName).Unwrap();  // CreateInstanceFrom returns a System.Runtime.Remoting.ObjectHandle.  According to MSDN, this is 
                                                            // an object that is a wrapper for the new instance, or null if the type is not found.  The return
                                                            // value needs to be unwrapped to access the real object.
                                                            // 
                                                            // http://msdn.microsoft.com/en-us/library/2xkww633(v=vs.110).aspx

                    namespaces = loader.LoadAssemblies(assemblyLocations, topLevelNamespace);
                }

                return namespaces;
            }
            finally
            {
                AppDomain.Unload(childDomain);
            }
        }
        #endregion
 
        #region Private Methods
        /// <summary>
        /// Creates a new AppDomain based on the parent AppDomains' Evidence and AppDomainSetup
        /// </summary>
        /// <param name="parentDomain">The parent AppDomain</param>
        /// <returns>A newly created AppDomain</returns>
        private AppDomain BuildChildDomain(AppDomain parentDomain)
        {
            Evidence evidence = new Evidence(parentDomain.Evidence);
            AppDomainSetup setup = parentDomain.SetupInformation;
            return AppDomain.CreateDomain("MainProc.ChildAppDomain", evidence, setup);
        }
        #endregion
 
 
        /// <summary>
        /// Remotable AssemblyLoader, this class inherits from <c>MarshalByRefObject</c> 
        /// to allow the CLR to marshall this object by reference across AppDomain boundaries
        /// </summary>
        class AssemblyLoader : MarshalByRefObject
        {
            #region Private/Internal Methods
            /// <summary>
            /// ReflectionOnlyLoad of single Assembly based on 
            /// the assemblyPath parameter
            /// </summary>
            /// <param name="assemblyLocations"></param>
            /// <returns></returns>
            [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
            internal List<String> LoadAssemblies(List<FileInfo> assemblyLocations, string topLevelNamespace)
            {
                var namespaces = new List<String>();

                try
                {
                    foreach (FileInfo assemblyLocation in assemblyLocations)
                    {
                        System.Diagnostics.Debug.WriteLine(assemblyLocation.FullName);
                        Assembly.LoadFrom(assemblyLocation.FullName);
                    }
 
                    foreach (Assembly hostedAssembly in AppDomain.CurrentDomain.GetAssemblies())
                    {
                        foreach (Type type in hostedAssembly.GetTypes())
                        {
                            if ((type.Namespace != null) && (type.Namespace.StartsWith(topLevelNamespace)))
                            {
                                namespaces.Add(hostedAssembly.GetName().Name + ": " + type.FullName);
                            }
                        }                   
                    }

                    return namespaces;
                }
                catch (FileNotFoundException)
                {
                    // Just return the namespaces collected to this point.
                    //
                    return namespaces;
                }
            }
            #endregion
        }
    }
}

 

The assemblies loaded by the LoadAssemblies method are loaded from C:\windows\temp.  They are PortableRandomAdder.dll and PortableRandomAdderCopy.dll (a copy of the original).  These assemblies each contain a very simple method that takes a single parameter of type int.  The method creates a random number and adds it to the integer value passed into the method.  This method is called AddToRandom.  You can download the source for PortableRandomAdder, and the source for this tutorial using the links at the bottom of the page.

The constructs described above need to be invoked.  Since this is a console application, the Main entry point defined in Program.cs will handle the necessary setup and invocation.  The source is shown below.

 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace AppDomainLoadUnloadEx2
{
    class Program
    {
        static void Main(string[] args)
        {
            SeparateAppDomainAssemblyLoader appDomainAssemblyLoader = new SeparateAppDomainAssemblyLoader();

            List<FileInfo> assemblies = new List<FileInfo>();
            string root = @"C:\windows\temp\";

            assemblies.Add(new FileInfo(root + @"PortableRandomAdder.dll"));
            assemblies.Add(new FileInfo(root + @"PortableRandomAdderCopy.dll"));

            Console.WriteLine();
            Console.WriteLine("Loading assemblies to parent AppDomain ...");

            foreach (FileInfo assemblyLocation in assemblies)
            {
                System.Diagnostics.Debug.WriteLine(assemblyLocation.FullName);
                Assembly.LoadFrom(assemblyLocation.FullName);
            }

            foreach (Assembly hostedAssembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                foreach (Type type in hostedAssembly.GetTypes())
                {
                    if ((type.Namespace != null) && (type.Namespace.StartsWith("PortableRandomAdder")))
                    {
                        Console.WriteLine("Namespace found: {0}", hostedAssembly.GetName().Name + ": " + type.FullName);
                    }
                }
            }

            Console.WriteLine();
            Console.WriteLine("Loading assemblies to child AppDomain ...");

            foreach (String @namespace in appDomainAssemblyLoader.LoadAssemblies(assemblies, "PortableRandomAdder"))
            {
                Console.WriteLine("Namespace found : {0}", @namespace);
            }

            Console.WriteLine();
            Console.WriteLine("Press any key to close ...");
            Console.ReadLine();

        }
    }
}

 

What is happening in the Main method is at first, a set of two assemblies are loaded in the default AppDomain.  This is to eventually have a set of types to compare exact matches with once the same exact types are loaded in a child AppDomain.  A subtle point worth noting is the assemblies loaded have class names that match their types.  They have the same exact methods, but the containing types of the AddToRandom methods are simply named to have a set of two different types to which we compare a set of two of these exact same types in the child AppDomain.  

 

Console Output From Example Process

Console Output from Example Process

 

The console output confirms two types are loaded by the parent AppDomain and the exact same types are loaded in the child AppDomain.  No runtime errors result.  In any give assembly, if you were to create a class of the same name as another class, both within the same namespace, a compile-time error would result mentioning the namespace already contains a definition for the type you are trying to create.  AppDomains allow for multiple identical types to exist happily within the same process.  Perfect for exposing your application functionality in an shiny new API third party developers can use.  Happy coding!

Download the example. | Download sample assembly solution, the sample assembly loaded in this example (PortableRandomAdder).