Implementing Dynamic Assembly Loading and Unloading in .NET Framework
This article demonstrates how to achieve dynamic assembly loading and unloading in a .NET Framework Windows Forms application, enabling runtime updates to DLL functionality without restarting the main application.
Core Concepts: AppDomain
The AppDomain (Application Domain) is a fundamental .NET concept that provides an isolation boundary for executing code. It acts as a lightweight container for applications, offering several key benefits:
- Isolation: Different
AppDomaininstances can load distinct assemblies, preventing inetrference. Exceptions or crashes in one domain do not affect others. - Security:
AppDomainallows for granular control over code execution permissions through security policies, enhancing application robustness. - Resource Management: Developers can monitor and manage resource consumption (memory, threads) within each
AppDomain, aiding in performance tuning and leak detection. - Dynamic Updates:
AppDomainfacilitates the unloading and reloading of assemblies with out terminating the entire application, crucial for scenarios requiring live updates. - Concurrency: While not a direct concurrency mechanism, multiple
AppDomains can be used to run application components in parallel, leveraging multi-core processors.
This implementation focuses on achieving these dynamic update capabilities within the .NET Framework environment.
Assembly Loading and Unloading Implementation
We will create a WinForms project, named HotfixTest, to illustrate the process. The core logic will reside in custom classes designed for managing assemblies.
AssemblyLoader Class
This class is responsible for loading an assembly and managing its types and instances. It enherits from MarshalByRefObject to facilitate cross-AppDomain communication.
using System;
using System.Collections.Generic;
using System.Reflection;
public class AssemblyLoader : MarshalByRefObject
{
private Assembly loadedAssembly;
private readonly Dictionary<string, object> classInstances = new Dictionary<string, object>();
public void LoadAssemblyFromPath(string assemblyPath)
{
loadedAssembly = Assembly.LoadFrom(assemblyPath);
classInstances.Clear();
}
public object GetOrCreateInstance(string fullTypeName)
{
if (loadedAssembly == null)
{
Console.WriteLine("[GetOrCreateInstance] Assembly not loaded.");
return null;
}
if (classInstances.TryGetValue(fullTypeName, out object existingInstance))
return existingInstance;
Type type = loadedAssembly.GetType(fullTypeName);
if (type == null)
{
Console.WriteLine($"[GetOrCreateInstance] Type not found: {fullTypeName}");
return null;
}
object newInstance = Activator.CreateInstance(type);
classInstances.Add(fullTypeName, newInstance);
return newInstance;
}
public object ExecuteMethod(string fullTypeName, string methodName, object[] methodParameters)
{
if (loadedAssembly == null)
{
Console.WriteLine("[ExecuteMethod] Assembly not loaded.");
return null;
}
Type type = loadedAssembly.GetType(fullTypeName);
if (type == null)
{
Console.WriteLine($"[ExecuteMethod] Type not found: {fullTypeName}");
return null;
}
MethodInfo method = type.GetMethod(methodName);
if (method == null)
{
Console.WriteLine($"[ExecuteMethod] Method not found: {methodName} in type {fullTypeName}");
return null;
}
object targetInstance;
if (!method.IsStatic)
{
targetInstance = GetOrCreateInstance(fullTypeName);
if (targetInstance == null) return null;
}
else
{
targetInstance = null; // For static methods
}
return method.Invoke(targetInstance, methodParameters);
}
}
AssemblyManager Class
This class orchestrates the creation and management of AppDomains and uses AssemblyLoader to interact with assemblies loaded within them.
using System;
using System.IO;
public class AssemblyManager
{
private AppDomain isolatedAppDomain;
private AssemblyLoader assemblyLoaderProxy;
public void LoadAssemblyInNewDomain(string assemblyPath)
{
if (!File.Exists(assemblyPath) || Path.GetExtension(assemblyPath).ToLower() != ".dll")
{
Console.WriteLine("Invalid DLL path or file extension.");
return;
}
string domainName = Path.GetFileNameWithoutExtension(assemblyPath);
isolatedAppDomain = AppDomain.CreateDomain(domainName);
string loaderAssemblyName = typeof(AssemblyLoader).Assembly.FullName;
string loaderFullName = typeof(AssemblyLoader).FullName;
assemblyLoaderProxy = (AssemblyLoader)isolatedAppDomain.CreateInstanceAndUnwrap(loaderAssemblyName, loaderFullName);
assemblyLoaderProxy.LoadAssemblyFromPath(assemblyPath);
}
public object InvokeMethodOnLoadedAssembly(string fullTypeName, string methodName, object[] parameters)
{
return assemblyLoaderProxy?.ExecuteMethod(fullTypeName, methodName, parameters);
}
public object GetInstanceFromLoadedAssembly(string fullTypeName)
{
return assemblyLoaderProxy?.GetOrCreateInstance(fullTypeName);
}
public void UnloadAssemblyDomain()
{
if (isolatedAppDomain != null)
{
string domainName = isolatedAppDomain.FriendlyName;
AppDomain.Unload(isolatedAppDomain);
isolatedAppDomain = null;
assemblyLoaderProxy = null;
Console.WriteLine($"AppDomain '{domainName}' unloaded. Assembly unloaded.");
}
}
}
Tool Class
A simple utility class. Note: In a real-world scenario, avoid having the main application reference utility classes that are intended for the isolated domain, as this can lead to unintended assembly loading.
using System;
public static class UtilityTools
{
public static int PerformAddition(int operand1, int operand2)
{
int result = operand1 + operand2;
Console.WriteLine($"UtilityTools Addition Result: {result}");
return result;
}
}
Dynamic Assembly Example
Create a separate Class Library project (e.g., DynamicPlugin) to serve as the dynamically loaded DLL. This library will contain the code that can be updated independently.
DynamicPlugin/Class1.cs:
using System;
using System.Threading.Tasks;
namespace DynamicPlugin
{
public class PluginClass
{
public string PluginName { get; set; }
public string SetPluginName(string name)
{
PluginName = name;
Console.WriteLine($"Plugin name set to: {PluginName}");
return "Name set successfully.";
}
public void GreetPluginUser()
{
Console.WriteLine($"{PluginName} says: Hello from the plugin!");
}
public void StartBackgroundProcess()
{
ProcessDataContinuously();
}
private bool keepProcessing = true;
private async void ProcessDataContinuously()
{
if (!keepProcessing) return;
while (keepProcessing)
{
await Task.Delay(1000);
Console.WriteLine("Plugin is processing...");
}
}
public void TestUtilityAddition()
{
int calculationResult = UtilityTools.PerformAddition(10, 15);
Console.WriteLine($"[TestUtilityAddition] Result from UtilityTools: {calculationResult}");
}
}
}
This PluginClass demonstrates instance-based methods and the use of static utility methods. The AssemblyLoader correctly handles both scenarios.
Integrating and Demonstrating Functionality
- Build the
DynamicPluginproject and copy the resultingDynamicPlugin.dllinto theHotfixTestproject's output directory (e.g.,bin/Debug). - Modify the
HotfixTestform's code-behind to utilize theAssemblyManager.
HotfixTest/Form1.cs:
using System;
using System.Windows.Forms;
namespace HotfixTest
{
public partial class Form1 : Form
{
private readonly AssemblyManager assemblyManager = new AssemblyManager();
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// Initialize UI elements if needed
}
// Button to Load DLL
private void LoadDllButton_Click(object sender, EventArgs e)
{
string pluginDllPath = System.IO.Path.Combine(Application.StartupPath, "DynamicPlugin.dll");
assemblyManager.LoadAssemblyInNewDomain(pluginDllPath);
assemblyManager.InvokeMethodOnLoadedAssembly("DynamicPlugin.PluginClass", "SetPluginName", new object[] { "MyDynamicPlugin" });
Console.WriteLine("Plugin loaded and name set.");
}
// Button to Unload DLL
private void UnloadDllButton_Click(object sender, EventArgs e)
{
assemblyManager.UnloadAssemblyDomain();
}
// Button to Call Method 1
private void CallMethod1Button_Click(object sender, EventArgs e)
{
assemblyManager.InvokeMethodOnLoadedAssembly("DynamicPlugin.PluginClass", "GreetPluginUser", null);
}
// Button to Call Method 2
private void CallMethod2Button_Click(object sender, EventArgs e)
{
assemblyManager.InvokeMethodOnLoadedAssembly("DynamicPlugin.PluginClass", "StartBackgroundProcess", null);
}
// Button to Call Method 3
private void CallMethod3Button_Click(object sender, EventArgs e)
{
assemblyManager.InvokeMethodOnLoadedAssembly("DynamicPlugin.PluginClass", "TestUtilityAddition", null);
}
}
}
Running the Application
When the HotfixTest application runs:
- Clicking the "Load DLL" button will create a new
AppDomain, loadDynamicPlugin.dllinto it, instantiatePluginClass, set its name, and report success. - Subsequent calls to "Call Method 1", "Call Method 2", and "Call Method 3" will execute the corresponding methods within the isolated
AppDomain. - Clicking "Unload DLL" will unload the
AppDomain, effectively removing the loaded assembly and terminating any processes running within it (like the background timer).
Dynamic Updates:
After unloading the assembly, you can modify the logic within DynamicPlugin/Class1.cs, rebuild the DLL, replace the existing DynamicPlugin.dll in the HotfixTest output directory, and then reload it. The application will then execute the updated logic without requiring a full application restart.