Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Dynamic Assembly Loading and Unloading in .NET Framework

Tech May 15 1

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:

  1. Isolation: Different AppDomain instances can load distinct assemblies, preventing inetrference. Exceptions or crashes in one domain do not affect others.
  2. Security: AppDomain allows for granular control over code execution permissions through security policies, enhancing application robustness.
  3. Resource Management: Developers can monitor and manage resource consumption (memory, threads) within each AppDomain, aiding in performance tuning and leak detection.
  4. Dynamic Updates: AppDomain facilitates the unloading and reloading of assemblies with out terminating the entire application, crucial for scenarios requiring live updates.
  5. 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

  1. Build the DynamicPlugin project and copy the resulting DynamicPlugin.dll into the HotfixTest project's output directory (e.g., bin/Debug).
  2. Modify the HotfixTest form's code-behind to utilize the AssemblyManager.

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:

  1. Clicking the "Load DLL" button will create a new AppDomain, load DynamicPlugin.dll into it, instantiate PluginClass, set its name, and report success.
  2. Subsequent calls to "Call Method 1", "Call Method 2", and "Call Method 3" will execute the corresponding methods within the isolated AppDomain.
  3. 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.