Loading Assemblies from Anywhere into a New AppDomain






4.95/5 (18 votes)
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 acrossAppDomain
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 thisAppDomain
(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 typeAssembly
and returns a result to the caller (see the example below). - Added a proxy manager that loads assemblies into
AppDomains
, performs queries and unloadsAppDomains
.
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 sameAppDomain
with the specified assembly. You can still unload the assembly by callingUnloadDomain
. -
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'sAppDomain
.
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
- How to load an assembly at runtime that is located in a folder that is not the bin folder of the application
- How to Load assembly to AppDomain with all references recursively?
History
- 5th September, 2012: Initial version