Recently a project I was working on required us to dynamically load an assembly at runtime, and invoke a method on the newly loaded assembly. After we had this working, I
noticed that when we tried to then load a newer version of the dynamic assembly, the newer bits weren't being picked up, and old code was executing. Time to investigate a
bit further:
I came across a great article by Jon Shemitz that pointed me in the right direction.
AppDomains are a powerful concept in .NET, however as most applications you write live within a single AppDomain, you may not have noticed them before. My problem was that
in .NET, you cannot simply unload an assembly directly. However, if we were to create a new AppDomain, load our dynamic assembly within it, call the methods needed, and
then unload the AppDomain, we could load, unload, update versions and reload until our hearts content. The final piece of our puzzle (that we already had in place) is to
use an interface to allow us the ability to invoke methods on the loaded assembly from our manager executable.
The solution I've created to demonstrate this concept uses four projects:
AppDomainLoad: A basic console application that simply news up an instance of the Manager class and calls the entry method.
WorkerShared: This project holds any objects that will be used by both the Manager and Worker classes. This is the only assembly that will be loaded by both AppDomains.
Our shared interface, IWorker, lives in this project. A utility class calls
AppDomain.CurrentDomain.GetAssemblies() to let us know what really is being loaded at runtime. This is a handy method to use, even in standard single AppDomain
applications, to ensure you only load what you need.
WorkerManager: The .dll that creates a separate AppDomain, loads an instance of the dynamic assembly, and casts this instance to the IWorker interface. After all this, the
Manager simply calls a method available from the interface. Once the work is complete, the AppDomain is unloaded, which drops any assemblies loaded within the worker
domain.
DisconnectedWorker: Where the rubber hits the road, this project executes the actual work. The worker class a) implements the IWorker interface b) derives from
MarshalByRefObject, which allows access to objects across domain boundaries (because these interactions are basically remoting under the covers), and c) is marked as
Serializable.
One final note, after building the projects, I placed the WorkerShared.dll and DisconnectedWorker.dll bits into the C:\BlogProjects\AssemblyPool directory. This is only to
simplify the project code; your project can locate these assemblies however you need. For this example, create the directory and drop the assemblies.
Here's the example code. The key classes' code is shown below. Let me know if you
have any questions.
IWorker interface
namespace WorkerShared
{
public interface IWorker
{
// Define required methods:
void DoWork();
}
}
WorkerManager
using System;
using WorkerShared;
namespace WorkerManager
{
public class Manager
{
private const string CONFIG_ASSEMBLY_POOL = @"C:\BlogProjects\AssemblyPool";
private const string CONFIG_DYNAMIC_ASSEMBLY_PROJECT = "DisconnectedWorker";
private const string CONFIG_DYNAMIC_ASSEMBLY_FULLY_QUALIFIED_NAME = "DisconnectedWorker.Worker";
private const string FORMAT_WORKER_DOMAIN_FRIENDLY_NAME = "Dynamic Worker Domain";
private const string FORMAT_WORKER_DOMAIN_CREATED = "Created '{0}' AppDomain";
private const string FORMAT_WORKER_DOMAIN_UNLOADED = "Unloaded '{0}' AppDomain";
private const string FORMAT_WORK_COMPLETE = "All work complete.";
private const string FORMAT_START_ASSEMBLIES = "Starting Assemblies Loaded:";
private const string FORMAT_END_ASSEMBLIES = "Post-unload Assemblies Loaded:";
public void RunAppDomainExample()
{
// Show current assemblies before we start:
Console.WriteLine(FORMAT_START_ASSEMBLIES);
Utilities.WriteCurrentLoadedAssemblies();
// create display name for appDomain
string workerName = string.Format(FORMAT_WORKER_DOMAIN_FRIENDLY_NAME);
// Construct and setup appDomain settings:
AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = CONFIG_ASSEMBLY_POOL;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
// Create domain
Console.WriteLine();
AppDomain workerAppDomain = AppDomain.CreateDomain(workerName, null, ads);
Console.WriteLine(FORMAT_WORKER_DOMAIN_CREATED, workerName)
;
// do work on proxy
IWorker workerInstance =
(IWorker)
workerAppDomain.CreateInstanceAndUnwrap(CONFIG_DYNAMIC_ASSEMBLY_PROJECT,
&nbs
p; CONFIG_D
YNAMIC_ASSEMBLY_FULLY_QUALIFIED_NAME);
// Execute the task by invoking method on the interface instance
workerInstance.DoWork();
// Unload worker appDomain
AppDomain.Unload(workerAppDomain);
Console.WriteLine(FORMAT_WORKER_DOMAIN_UNLOADED, workerName)
;
Console.WriteLine();
// Show current assemblies before we start:
Console.WriteLine(FORMAT_END_ASSEMBLIES);
Utilities.WriteCurrentLoadedAssemblies();
Console.WriteLine(FORMAT_WORK_COMPLETE);
Console.ReadLine();
}
}
}
DisconnectedWorker
using System;
using WorkerShared;
namespace DisconnectedWorker
{
[Serializable]
public class Worker : MarshalByRefObject, IWorker
{
public void DoWork()
{
// Show the assemblies loaded in this appDomain
Utilities.WriteCurrentLoadedAssemblies();
}
}
}
Utilities
using System;
using System.Reflection;
namespace WorkerShared
{
public static class Utilities
{
public static void WriteCurrentLoadedAssemblies()
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
Console.WriteLine("Loaded:
{0}.", assembly.ManifestModule.Name);
}
}
}
}
Payday: The Project Output
Starting Assemblies Loaded:
Loaded: mscorlib.dll.
Loaded: Microsoft.VisualStudio.HostingProcess.Utilities.dll.
Loaded: System.Windows.Forms.dll.
Loaded: System.dll.
Loaded: System.Drawing.dll.
Loaded: Microsoft.VisualStudio.HostingProcess.Utilities.Sync.dll.
Loaded: AppDomainLoad.vshost.exe.
Loaded: System.Data.dll.
Loaded: System.Xml.dll.
Loaded: AppDomainLoad.exe.
Loaded: WorkerManager.dll.
Loaded: WorkerShared.dll.
Created 'Dynamic Worker Domain' AppDomain
Loaded: mscorlib.dll.
Loaded: Microsoft.VisualStudio.HostingProcess.Utilities.dll.
Loaded: DisconnectedWorker.dll.
Loaded: WorkerShared.dll.
Unloaded 'Dynamic Worker Domain' AppDomain
Post-unload Assemblies Loaded:
Loaded: mscorlib.dll.
Loaded: Microsoft.VisualStudio.HostingProcess.Utilities.dll.
Loaded: System.Windows.Forms.dll.
Loaded: System.dll.
Loaded: System.Drawing.dll.
Loaded: Microsoft.VisualStudio.HostingProcess.Utilities.Sync.dll.
Loaded: AppDomainLoad.vshost.exe.
Loaded: System.Data.dll.
Loaded: System.Xml.dll.
Loaded: AppDomainLoad.exe.
Loaded: WorkerManager.dll.
Loaded: WorkerShared.dll.
All work complete.