Click here to Skip to main content
15,885,842 members
Articles / Programming Languages / C#

Loading Assemblies from Anywhere into a New AppDomain

Rate me:
Please Sign up or sign in to vote.
4.95/5 (19 votes)
5 Sep 2012CPOL3 min read 95.1K   64   20
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.

C#
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:

  • C#
    bool LoadAssembly(string assemblyPath, string domainName).

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

  • C#
    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.

  • C#
    bool UnloadDomain(string domainName)

    Unloads an application domain from the process.

  • C#
    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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Visma Software
Romania Romania
Marius Bancila is the author of Modern C++ Programming Cookbook and The Modern C++ Challenge. He has been a Microsoft MVP since 2006, initially for VC++ and nowadays for Development technologies. He works as a system architect for Visma, a Norwegian-based company. He works with various technologies, both managed and unmanaged, for desktop, cloud, and mobile, mainly developing with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. You can follow Marius on Twitter at @mariusbancila.

Comments and Discussions

 
QuestionBeautiful piece of code. Sooooo helpful! Pin
Member 1596123924-Mar-23 17:12
Member 1596123924-Mar-23 17:12 
QuestionConverting the code to VB.NET gives an error Pin
Member 802512724-May-18 7:28
Member 802512724-May-18 7:28 
When converting the code to VB.NET, I assume that this code it 2 separate Classes (1 for AssemblyReflectionProxy and 1 for AssemblyReflectionManager - correct me if I'm wrong)
I get a
BC32016	'Public Overloads ReadOnly Property Count As Integer' has no parameters and its return type cannot be indexed.
at line 60 of the AssemblyReflectionManager Class

VB code:
Dim count As Integer = _loadedAssemblies.Values.Count(Function(a) a Is appDomain)


C# code:
int count = _loadedAssemblies.Values.Count(a => a == appDomain);


Can anyone help me with this?

modified 24-May-18 13:49pm.

QuestionLoading Dependent Assemblies Pin
ACordner27-Oct-17 11:30
ACordner27-Oct-17 11:30 
QuestionReflect always return null Pin
Bradwen7-Nov-14 0:52
Bradwen7-Nov-14 0:52 
AnswerRe: Reflect always return null Pin
Bradwen7-Nov-14 0:59
Bradwen7-Nov-14 0:59 
QuestionAwesome Pin
Sing Abend7-Apr-14 10:20
professionalSing Abend7-Apr-14 10:20 
QuestionHow to use the assembly loaded in the demodomain ? Pin
loic stein5-Jul-13 2:30
loic stein5-Jul-13 2:30 
QuestionTranslate Reflect routines to _NET C++ Pin
Member 70938127-Sep-12 3:03
Member 70938127-Sep-12 3:03 
AnswerRe: Translate Reflect routines to _NET C++ Pin
Marius Bancila27-Sep-12 3:25
professionalMarius Bancila27-Sep-12 3:25 
GeneralRe: Translate Reflect routines to _NET C++ Pin
Member 70938127-Sep-12 5:02
Member 70938127-Sep-12 5:02 
GeneralRe: Translate Reflect routines to _NET C++ Pin
Tarmo Pikaro6-Nov-12 9:11
Tarmo Pikaro6-Nov-12 9:11 
GeneralRe: Translate Reflect routines to _NET C++ Pin
Member 70938115-Nov-12 23:11
Member 70938115-Nov-12 23:11 
QuestionGetting an Exception Pin
Ajeeth25-Sep-12 0:05
Ajeeth25-Sep-12 0:05 
AnswerRe: Getting an Exception Pin
Marius Bancila25-Sep-12 0:13
professionalMarius Bancila25-Sep-12 0:13 
GeneralRe: Getting an Exception Pin
Ajeeth25-Sep-12 3:48
Ajeeth25-Sep-12 3:48 
GeneralRe: Getting an Exception Pin
Marius Bancila25-Sep-12 9:17
professionalMarius Bancila25-Sep-12 9:17 
GeneralRe: Getting an Exception Pin
Ajeeth25-Sep-12 19:47
Ajeeth25-Sep-12 19:47 
GeneralRe: Getting an Exception Pin
Ed_Lon20-Apr-19 15:52
Ed_Lon20-Apr-19 15:52 
QuestionThank you Pin
Dr. Glyph18-Sep-12 5:13
Dr. Glyph18-Sep-12 5:13 
GeneralNeat piece of code Pin
Espen Harlinn5-Sep-12 13:05
professionalEspen Harlinn5-Sep-12 13:05 

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

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