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.
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:
- Main(), scans the plug-in directory and creates a new temporary AppDomain for each plug-in that is found
- PluginCallback(), the temporary AppDomains callback method, in which the plug-in is loaded and executed
public class Program
{
static void Main()
{
try
{
foreach (var filePath in Directory.GetFiles(Constants.PluginPath,
Constants.PluginSearchPattern))
{
var pluginAppDomainSetup = new AppDomainSetup();
pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup);
var pluginContext = new PluginContext { FilePath = filePath };
plugInAppDomain.SetData(Constants.PluginContextKey, pluginContext);
plugInAppDomain.DoCallBack(PluginCallback);
pluginContext = plugInAppDomain.GetData(Constants.PluginContextKey) as PluginContext;
AppDomain.Unload(plugInAppDomain);
if (pluginContext != null && pluginContext.CanDeletePlugin)
{
File.Delete(filePath);
}
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
private static void PluginCallback()
{
try
{
var pluginContext = AppDomain.CurrentDomain.GetData(Constants.PluginContextKey)
as PluginContext;
if (pluginContext != null)
{
var pluginAssembly = Assembly.LoadFrom(pluginContext.FilePath);
foreach (var type in pluginAssembly.GetTypes())
{
if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type))
{
var plugin = Activator.CreateInstance(type) as IPlugin;
if (plugin != null)
{
plugin.Activate();
plugin.Execute();
plugin.Deactivate();
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
{
foreach (var filePath in Directory.GetFiles(Constants.PluginPath,
Constants.PluginSearchPattern))
{
var pluginAppDomainSetup = new AppDomainSetup();
pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
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;
foreach (var type in pluginAssembly.GetTypes())
{
if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type))
{
var plugin = Activator.CreateInstance(type) as IPlugin;
if (plugin != null)
{
plugin.Activate();
plugin.Execute();
plugin.Deactivate();
canDelete = true;
break;
}
}
}
AppDomain.Unload(plugInAppDomain);
if (canDelete)
{
File.Delete(filePath);
}
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
{
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.