Click here to Skip to main content
Click here to Skip to main content

Loading Assemblies from Anywhere into a New AppDomain

, 5 Sep 2012
Rate this:
Please Sign up or sign in to vote.
In this article I'm generalizing it and extending "Loading Assemblies in pearate Directories" with some helper functionality.

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 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 Sasha Barber's article and that was a game changer.

The Solution

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 instantiatign 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 Sasha 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 interface:

  • 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 can not 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Marius Bancila
Software Developer (Senior) Visma Software
Romania Romania
Marius Bancila is a Microsoft MVP for VC++. He works as a software developer for Visma, a Norwegian-based company. He is mainly focused on building desktop applications with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++ programmers.
Follow on   Twitter

Comments and Discussions

 
QuestionAwesome PinmemberSing Abend7-Apr-14 10:20 
QuestionHow to use the assembly loaded in the demodomain ? Pinmemberloic stein5-Jul-13 2:30 
QuestionTranslate Reflect routines to _NET C++ PinmemberMember 70938127-Sep-12 3:03 
AnswerRe: Translate Reflect routines to _NET C++ PinmemberMarius Bancila27-Sep-12 3:25 
GeneralRe: Translate Reflect routines to _NET C++ PinmemberMember 70938127-Sep-12 5:02 
GeneralRe: Translate Reflect routines to _NET C++ PinmemberTarmik6-Nov-12 9:11 
GeneralRe: Translate Reflect routines to _NET C++ PinmemberMember 70938115-Nov-12 23:11 
QuestionGetting an Exception PinmemberAjeeth25-Sep-12 0:05 
AnswerRe: Getting an Exception PinmemberMarius Bancila25-Sep-12 0:13 
GeneralRe: Getting an Exception PinmemberAjeeth25-Sep-12 3:48 
GeneralRe: Getting an Exception PinmemberMarius Bancila25-Sep-12 9:17 
GeneralRe: Getting an Exception PinmemberAjeeth25-Sep-12 19:47 
QuestionThank you PinmemberDr. Glyph18-Sep-12 5:13 
GeneralNeat piece of code PinmvpEspen Harlinn5-Sep-12 13:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 5 Sep 2012
Article Copyright 2012 by Marius Bancila
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid