Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Loading Assemblies from Anywhere into a New AppDomain

0.00/5 (No votes)
5 Sep 2012 1  
In this article, I'm generalizing and extending Loading Assemblies in pearate Directories with some helper functionality.

Introduction

Those of you that tried to load an assembly for examining with reflection into an appdomain that would be unloaded as soon as you're done with the reflection know the experience must be similar to walking on a magic realm where nothing works and everything you touch breaks. My problem was solved with a piece of code written by Sacha Barber in his article, Loading Assemblies in Separate Directories Into a New AppDomain. But his sample was too particular, so in this article, I'm generalizing it and extending it with some helper functionality.

The Problem

My problem with loading assemblies started when I was trying to do the following:

  • Read some information from various assemblies through reflection
  • The assemblies were located in various places on disk
  • The assemblies need to be loaded only to perform reflection, therefore
  • Each assembly would be loaded in a separate AppDomain so when examination finishes, they can be unloaded

As much as I tried, loading assemblies failed for various reasons, depending on the solution I tried. Eventually, I found Sacha Barber's article and that was a game changer.

The Solution

The solution to the problem is based on:

  • creating an assembly proxy (or wrapper), derived from MarshalByRefObject, so that the CLR can marshal it by reference across AppDomain boundaries
  • loading the assembly within this proxy (Assembly.ReflectionOnlyLoadFrom)
  • performing the reflection inside this proxy and return the data you need
  • creating a temporary AppDomain and instantiating the assembly proxy in this AppDomain (AppDomain.CreateInstanceFrom)
  • unloading the AppDomain as soon as you finished reflecting

However, you have to keep in mind that reflection on the assembly loaded this way is only possible inside the proxy (the one derived from MarshalByRefObject). It is not possible to return any "reflection object" (anything defined in the System.Reflection namespace, such as Type, MethodInfo, etc.). Trying to access these from another AppDomain (the caller's domain) would result in exceptions.

Generalization and Extensions

I have done two things with Sacha Barber's code:

  • Generalized the assembly proxy so that it can perform any reflection query on the assembly. Method Reflect() takes as argument any function with a parameter of type Assembly and returns a result to the caller (see the example below).
  • Added a proxy manager that loads assemblies into AppDomains, performs queries and unloads AppDomains.

Here is a simple example for using this manager.

var assemblyPath = "..."; // your assembly path here
var manager = new AssemblyReflectionManager();

var success = manager.LoadAssembly(assemblyPath, "demodomain");

var results = manager.Reflect(assemblyPath, (a) =>{
   var names = new List<string>();
   var types = a.GetTypes();
   foreach (var t in types)
      names.Add(t.Name);
   return names;
});

foreach(var name in results)
  Console.WriteLine(name);
  
manager.UnloadAssembly(assemblyPath);

The AssemblyReflectionManager contains the following public interfaces:

  • bool LoadAssembly(string assemblyPath, string domainName).

    Loads an assembly into an application domain. This function fails if the assembly path was already loaded.

  • bool UnloadAssembly(string assemblyPath)

    Unloads an already loaded assembly, by unloading the AppDomain in which it was loaded. This function fails if there are more assemblies loaded in the same AppDomain with the specified assembly. You can still unload the assembly by calling UnloadDomain.

  • bool UnloadDomain(string domainName)

    Unloads an application domain from the process.

  • TResult Reflect<TResult>(string assemblyPath, Func<Assembly, TResult> func)

    Performs reflection on a loaded assembly and returns the result. It is not possible to return any type from the System.Reflection namespace, as they are not valid outside the proxy's AppDomain.

The Code

The code (shorted of comments) for the proxy and the manager is available below.

public class AssemblyReflectionProxy : MarshalByRefObject
{
  private string _assemblyPath;

  public void LoadAssembly(String assemblyPath)
  {
     try
     {
        _assemblyPath = assemblyPath;
        Assembly.ReflectionOnlyLoadFrom(assemblyPath);
     }
     catch (FileNotFoundException)
     {
        // Continue loading assemblies even if an assembly 
        // cannot be loaded in the new AppDomain.
     }
  }

  public TResult Reflect<TResult>(Func<Assembly, TResult> func)
  {
     DirectoryInfo directory = new FileInfo(_assemblyPath).Directory;
     ResolveEventHandler resolveEventHandler =
         (s, e) =>
         {
            return OnReflectionOnlyResolve(
                e, directory);
         };

     AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;

     var assembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault
                    (a => a.Location.CompareTo(_assemblyPath) == 0);

     var result = func(assembly);

     AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;

     return result;
  }

  private Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
  {
     Assembly loadedAssembly =
         AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies()
             .FirstOrDefault(
               asm => string.Equals(asm.FullName, args.Name,
                   StringComparison.OrdinalIgnoreCase));

     if (loadedAssembly != null)
     {
        return loadedAssembly;
     }

     AssemblyName assemblyName =
         new AssemblyName(args.Name);
     string dependentAssemblyFilename =
         Path.Combine(directory.FullName,
         assemblyName.Name + ".dll");

     if (File.Exists(dependentAssemblyFilename))
     {
        return Assembly.ReflectionOnlyLoadFrom(
            dependentAssemblyFilename);
     }
     return Assembly.ReflectionOnlyLoad(args.Name);
  }
}  

   public class AssemblyReflectionManager : IDisposable
   {
      Dictionary<string, AppDomain> _mapDomains = new Dictionary<string, AppDomain>();
      Dictionary<string, AppDomain> _loadedAssemblies = new Dictionary<string, AppDomain>();
      Dictionary<string, AssemblyReflectionProxy> _proxies = 
                         new Dictionary<string, AssemblyReflectionProxy>();

      public bool LoadAssembly(string assemblyPath, string domainName)
      {
         // if the assembly file does not exist then fail
         if (!File.Exists(assemblyPath))
            return false;

         // if the assembly was already loaded then fail
         if (_loadedAssemblies.ContainsKey(assemblyPath))
         {
            return false;
         }

         // check if the appdomain exists, and if not create a new one
         AppDomain appDomain = null;
         if (_mapDomains.ContainsKey(domainName))
         {
            appDomain = _mapDomains[domainName];
         }
         else
         {
            appDomain = CreateChildDomain(AppDomain.CurrentDomain, domainName);
            _mapDomains[domainName] = appDomain;
         }

         // load the assembly in the specified app domain
         try
         {
            Type proxyType = typeof(AssemblyReflectionProxy);
            if (proxyType.Assembly != null)
            {
               var proxy =
                   (AssemblyReflectionProxy)appDomain.
                       CreateInstanceFrom(
                       proxyType.Assembly.Location,
                       proxyType.FullName).Unwrap();

               proxy.LoadAssembly(assemblyPath);

               _loadedAssemblies[assemblyPath] = appDomain;
               _proxies[assemblyPath] = proxy;

               return true;
            }
         }
         catch
         {}

         return false;
      }

      public bool UnloadAssembly(string assemblyPath)
      {
         if (!File.Exists(assemblyPath))
            return false;

         // check if the assembly is found in the internal dictionaries
         if (_loadedAssemblies.ContainsKey(assemblyPath) &&

            _proxies.ContainsKey(assemblyPath))
         {
            // check if there are more assemblies loaded in the same app domain; 
            // in this case fail
            AppDomain appDomain = _loadedAssemblies[assemblyPath];
            int count = _loadedAssemblies.Values.Count(a => a == appDomain);
            if (count != 1)
               return false;

            try
            {
               // remove the appdomain from the dictionary and unload it from the process
               _mapDomains.Remove(appDomain.FriendlyName);
               AppDomain.Unload(appDomain);

               // remove the assembly from the dictionaries
               _loadedAssemblies.Remove(assemblyPath);
               _proxies.Remove(assemblyPath);

               return true;
            }
            catch
            {
            }
         }

         return false;
      }

      public bool UnloadDomain(string domainName)
      {
         // check the appdomain name is valid
         if (string.IsNullOrEmpty(domainName))
            return false;

         // check we have an instance of the domain
         if (_mapDomains.ContainsKey(domainName))
         {
            try
            {
               var appDomain = _mapDomains[domainName];

               // check the assemblies that are loaded in this app domain
               var assemblies = new List<string>();
               foreach (var kvp in _loadedAssemblies)
               {
                  if (kvp.Value == appDomain)
                     assemblies.Add(kvp.Key);
               }

               // remove these assemblies from the internal dictionaries
               foreach (var assemblyName in assemblies)
               {
                  _loadedAssemblies.Remove(assemblyName);
                  _proxies.Remove(assemblyName);
               }

               // remove the appdomain from the dictionary
               _mapDomains.Remove(domainName);

               // unload the appdomain
               AppDomain.Unload(appDomain);

               return true;
            }
            catch
            {
            }
         }

         return false;
      }

      public TResult Reflect<TResult>(string assemblyPath, Func<Assembly, TResult> func)
      {
         // check if the assembly is found in the internal dictionaries
         if (_loadedAssemblies.ContainsKey(assemblyPath) &&
            _proxies.ContainsKey(assemblyPath))
         {
            return _proxies[assemblyPath].Reflect(func);
         }

         return default(TResult);
      }

      public void Dispose()
      {
         Dispose(true);
         GC.SuppressFinalize(this);
      }

      ~AssemblyReflectionManager()
      {
         Dispose(false);
      }

      protected virtual void Dispose(bool disposing)
      {
         if (disposing)
         {
            foreach (var appDomain in _mapDomains.Values)
               AppDomain.Unload(appDomain);

            _loadedAssemblies.Clear();
            _proxies.Clear();
            _mapDomains.Clear();
         }
      }

      private AppDomain CreateChildDomain(AppDomain parentDomain, string domainName)
      {
         Evidence evidence = new Evidence(parentDomain.Evidence);
         AppDomainSetup setup = parentDomain.SetupInformation;
         return AppDomain.CreateDomain(domainName, evidence, setup);
      }
   }

Additional Readings

History

  • 5th September, 2012: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here