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 = "...";
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)
{
}
}
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 (!File.Exists(assemblyPath))
return false;
if (_loadedAssemblies.ContainsKey(assemblyPath))
{
return false;
}
AppDomain appDomain = null;
if (_mapDomains.ContainsKey(domainName))
{
appDomain = _mapDomains[domainName];
}
else
{
appDomain = CreateChildDomain(AppDomain.CurrentDomain, domainName);
_mapDomains[domainName] = appDomain;
}
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;
if (_loadedAssemblies.ContainsKey(assemblyPath) &&
_proxies.ContainsKey(assemblyPath))
{
AppDomain appDomain = _loadedAssemblies[assemblyPath];
int count = _loadedAssemblies.Values.Count(a => a == appDomain);
if (count != 1)
return false;
try
{
_mapDomains.Remove(appDomain.FriendlyName);
AppDomain.Unload(appDomain);
_loadedAssemblies.Remove(assemblyPath);
_proxies.Remove(assemblyPath);
return true;
}
catch
{
}
}
return false;
}
public bool UnloadDomain(string domainName)
{
if (string.IsNullOrEmpty(domainName))
return false;
if (_mapDomains.ContainsKey(domainName))
{
try
{
var appDomain = _mapDomains[domainName];
var assemblies = new List<string>();
foreach (var kvp in _loadedAssemblies)
{
if (kvp.Value == appDomain)
assemblies.Add(kvp.Key);
}
foreach (var assemblyName in assemblies)
{
_loadedAssemblies.Remove(assemblyName);
_proxies.Remove(assemblyName);
}
_mapDomains.Remove(domainName);
AppDomain.Unload(appDomain);
return true;
}
catch
{
}
}
return false;
}
public TResult Reflect<TResult>(string assemblyPath, Func<Assembly, TResult> func)
{
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