Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Load and Unload Plug-in without File Lock

0.00/5 (No votes)
24 Mar 2015 1  
This article describes a solution that allows an application to load and execute a plug-in, and unloading it without a file lock on the assembly.

Introduction

Using plug-ins to extend the logic of an application is a good example of code abstraction. The plug-in interface describes the functionality, and the implementation is decoupled from the application, except for the plug-in interface. Furthermore, the loading and unloading of the plug-in at runtime is a powerful feature. Many articles I found on the internet describe how a plug-in can be loaded into a temporary AppDomain. But most of them result in a situation in which the plug-in is actually kept in the application's AppDomain, and thus cannot be unloaded, resulting in a file lock on the plug-in assembly on the hard-disk. This article describes a way how a plug-in can be used and released without having a file lock to the plug-in after the unload.

Background

Lately, I worked on a project that uses a plug-in framework to update another system. The application would check for newer plug-in versions, and download them when they became available. Once downloaded, the plug-in would be executed to update the running system. A drawback of the system was that it had to restart itself to unload the previous plug-in before the newest version could be used.

The root of the problem was that a temporary AppDomain was created for the plug-in, but when it was loaded, it was still linked to the application's AppDomain. Even though the system used a separate AppDomain for the plug-in, when it was unloaded, the loaded plug-in was still locking the assembly on the drive and could not be deleted, because it was actually loaded into the application's AppDomain. The deletion of the previous plug-in version was one of the system's requirements to ensure that the system would use the latest version. This unexpected file lock raised the suspicion that the AppDomain usage was incorrect.

I found the solution to my problem in the answer to this question here:

https://stackoverflow.com/questions/425077/how-to-delete-the-pluginassembly-after-appdomain-unloaddomain/2475177#2475177.

Using the code

The sample application will load and execute a plug-in from a defined folder. The plug-in is loaded and executed in a temporary AppDomain that is separately created for each plug-in that is found in the given folder. The plug-in loading and execution is done in the seperate plug-in AppDomain context. After it has been executed, the plug-in is deleted from the hard-disk. This is done to show that the application has no file lock on the assembly, after its temporary AppDomain is unloaded.

The Implementation

The following sections describe the implementation of the sample code:

  • Class Diagram: a UML overview, describing the packages (assemblies), classes and their relationships
  • PluginBase: the base assembly, containing the plug-in interface description
  • PlugIn1: a sample plug-in implementation
  • Application: contains the plug-in loading and execution routines

Class Diagram

The sample consists of a console application, the plug-in interface and a sample plug-in implementation. The following class diagram shows the arrangement of the interface and the classes in their assemblies.

Image 1

PluginBase

The PluginBase assembly contains the interface definition of the plug-ins.

public interface IPlugin
{
    void Activate();
    void Execute();
    void Deactivate();
}

Plugin1

A demonstration plug-in that shows the method calls by displaying a message box.

using System.Windows.Forms;
using PluginBase;

public class Plugin1 : IPlugin
{
    public void Activate()
    {
        MessageBox.Show("Activating Plugin 1");
    }

    public void Execute()
    {
        MessageBox.Show("Peforming Action Plugin 1");
    }

    public void Deactivate()
    {
        MessageBox.Show("Deactivating Plugin 1");
    }
}

PluginContext

The plug-in context class that is used as an example for cross AppDomain data transfer. It contains the plug-in's file path, from where it is to be loaded, and the CanDeletePlugin flag to return the state whether the plug-in can be deleted from the hard-disk. Note that the class has to be Serializable in order to be exchangeable across AppDomain boundaries.

using System;

[Serializable]
public class PluginContext
{
    public string FilePath { get; set; }

    public bool CanDeletePlugin { get; set; }
}

Application

The test application which contains two methods:

  1. Main(), scans the plug-in directory and creates a new temporary AppDomain for each plug-in that is found
  2. PluginCallback(), the temporary AppDomains callback method, in which the plug-in is loaded and executed
public class Program
{
    static void Main()
    {
        try
        {
            // Iterate through all plug-ins.
            foreach (var filePath in Directory.GetFiles(Constants.PluginPath,
                                                        Constants.PluginSearchPattern))
            {
                // Create the plug-in AppDomain setup.
                var pluginAppDomainSetup = new AppDomainSetup();
                pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

                // Create the plug-in AppDomain with the setup.
                var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup);

                // Pass the plug-in file path to the AppDomain
                var pluginContext = new PluginContext { FilePath = filePath };
                plugInAppDomain.SetData(Constants.PluginContextKey, pluginContext);

                // Execute the loader in the plug-in AppDomain's context.
                // This will also execute the plug-in.
                plugInAppDomain.DoCallBack(PluginCallback);

                // Retrieve the flag if the plug-in has executed and can be deleted.
                pluginContext = plugInAppDomain.GetData(Constants.PluginContextKey) as PluginContext;

                // Unload the plug-in AppDomain.
                AppDomain.Unload(plugInAppDomain);

                // Delete the plug-in if applicable.
                if (pluginContext != null && pluginContext.CanDeletePlugin)
                {
                    File.Delete(filePath);
                }
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }

    /// <summary>
    /// The callback routine that is executed in the plug-in AppDomain context.
    /// </summary>
    private static void PluginCallback()
    {
        try
        {
            // Retrieve the filePath from the plug-in AppDomain
            var pluginContext = AppDomain.CurrentDomain.GetData(Constants.PluginContextKey)
                                  as PluginContext;
            if (pluginContext != null)
            {
                // Load the plug-in.
                var pluginAssembly = Assembly.LoadFrom(pluginContext.FilePath);

                // Iterate through types of the plug-in assembly to find the plug-in class.
                foreach (var type in pluginAssembly.GetTypes())
                {
                    if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type))
                    {
                        // Create the instance of the plug-in and call the interface methods.
                        var plugin = Activator.CreateInstance(type) as IPlugin;
                        if (plugin != null)
                        {
                            plugin.Activate();
                            plugin.Execute();
                            plugin.Deactivate();

                            // Set the delete flag to true, to signal that the plug-in can be deleted.
                            pluginContext.CanDeletePlugin = true;
                            AppDomain.CurrentDomain.SetData(Constants.DeletePluginKey, pluginContext);
                            break;
                        }
                    }
                }
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }
}

Points of Interest

AppDomain.DoCallback(CrossAppDomainDelegate)

The AppDomain.DoCallback() method is the place where the magic happens. It will call the assigned callback routine in the temporary plug-in AppDomain context.

Plug-in Instantiation and Execution

After the plug-in assembly is loaded in the temporary AppDomain context, the plug-in class type is searched. Once found, the Activator class is used to create a new instance of the plug-in. After successful instantiation, the methods of the plug-in are called. Finally the CanDeletePlugin flag is set and returned to the main application by calling SetData().

AppDomain.SetData(string, object), AppDomain.GetData(string)

Because the plug-ins run in a separate AppDomain, the inter AppDomain communication has to be done using the SetData() and GetData() methods of the AppDomain. In this sample I am using the PluginContext class to show that the plug-in is executed in another AppDomain, and that the PluginContext class has to be serializable, so that is can be sent from one domain to the other.

The Failing Implementation

To be complete: the next code section describes the failing implementation. I have found this kind of implementation several times during my research on the internet. Please note that the plug-in part is working! The plug-in is loaded into memory in its own AppDomain, and after usage the temporary AppDomain is unloaded. But the file deletion fails because the plug-in is actually loaded into the application's domain.

public class Program
{
    static void Main()
    {
        try
        {
            // Iterate through all plug-ins.
            foreach (var filePath in Directory.GetFiles(Constants.PluginPath,
                                                        Constants.PluginSearchPattern))
            {
                // Create the plug-in AppDomain setup.
                var pluginAppDomainSetup = new AppDomainSetup();
                pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

                // Create the plug-in AppDomain with the setup.
                var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup);
                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;

                byte[] fileContent;
                using (FileStream dll = File.OpenRead(filePath))
                {
                    fileContent = new byte[dll.Length];
                    dll.Read(fileContent, 0, (int)dll.Length);
                }

                var pluginAssembly = plugInAppDomain.Load(fileContent);
                bool canDelete = false;

                // Iterate through types of the plug-in assembly to find the plug-in class.
                foreach (var type in pluginAssembly.GetTypes())
                {
                    if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type))
                    {
                        // Create the instance of the plug-in and call the interface methods.
                        var plugin = Activator.CreateInstance(type) as IPlugin;
                        if (plugin != null)
                        {
                            plugin.Activate();
                            plugin.Execute();
                            plugin.Deactivate();
                            canDelete = true;
                            break;
                        }
                    }
                }

                // Unload the plug-in AppDomain.
                AppDomain.Unload(plugInAppDomain);

                // Delete the plug-in if applicable.
                if (canDelete)
                {
                    File.Delete(filePath);
                }
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }

    private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
    {
       // Quick and dirty
       string fileName = args.Name.Split(new[] { ',' })[0];
       fileName = Path.ChangeExtension(fileName, "DLL");
       fileName = Path.Combine(Constants.PluginPath, fileName);
       fileName = Path.GetFullPath(fileName);
       return Assembly.LoadFile(fileName);
    }
}

Conclusion

This sample shows a way how a plug-in can be loaded and executed using a temporary AppDomain, without keeping a file lock on the assembly after unloading the temporary AppDomain.

I hope that this article provides you a complete scenario of how to use temporary AppDomains.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here