In this article, I’ll explain how to implement the plugin pattern. This approach uses a generic plugin loader that solves many real world problems when loading plugins in .NET.
Besides being generic, this plugin loader also solves the following real world problems when working with plugins:
- Loads assemblies that have dependencies.
- Load assemblies that have unmanaged dependencies (like C++ DLLs).
- Solves the “IsAssignableFrom incorrectly returns false” problem that happens in .NET Core.
- Unloads assemblies.
- Can pass in parameters to plugin constructors.
If you find that this generic plugin loader doesn’t solve a real world problem for you, go ahead and add the functionality yourself (or leave a comment explaining the problem). Note: This is an alternative to using MEF to load assemblies dynamically.
What are plugins?
To put this in concrete terms, in a plugin architecture you have three assemblies:
- An executable program that uses plugins.
- A DLL that defines a plugin interface.
- One or more assemblies that implement the plugin interface.
This approach is very flexible and allows you extend the program by plugging in new Plugin DLLs instead of having to modify the main program logic.
Next, I’ll explain step-by-step how to actually implement this in the code.
1 – Create the generic plugin loader
To create the generic plugin loader, implement two classes:
- A generic load context class. This loads assemblies.
- A generic plugin loader class that uses the context class when creating plugin instances.
First, create the generic plugin load context class by subclassing AssemblyLoadContext:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
public class GenericAssemblyLoadContext<T> : AssemblyLoadContext where T : class
{
private AssemblyDependencyResolver _resolver;
private HashSet<string> assembliesToNotLoadIntoContext;
public GenericAssemblyLoadContext(string pluginPath) : base(isCollectible: true)
{
var pluginInterfaceAssembly = typeof(T).Assembly.FullName;
assembliesToNotLoadIntoContext = GetReferencedAssemblyFullNames(pluginInterfaceAssembly);
assembliesToNotLoadIntoContext.Add(pluginInterfaceAssembly);
_resolver = new AssemblyDependencyResolver(pluginPath);
}
private HashSet<string> GetReferencedAssemblyFullNames(string ReferencedBy)
{
return AppDomain.CurrentDomain
.GetAssemblies().FirstOrDefault(t => t.FullName == ReferencedBy)
.GetReferencedAssemblies()
.Select(t => t.FullName)
.ToHashSet();
}
protected override Assembly Load(AssemblyName assemblyName)
{
//Do not load the Plugin Interface DLL into the adapter's context
//otherwise IsAssignableFrom is false.
if (assembliesToNotLoadIntoContext.Contains(assemblyName.FullName))
{
return null;
}
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
Code language: C# (cs)
Next, create the generic plugin loader class. This uses the generic plugin context class to load assemblies from a given directory. In each assembly, it looks for types that implement the plugin interface. It then creates instances of the plugin types, adds them to a list, and returns the list of plugin objects.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
public class GenericPluginLoader<T> where T : class
{
private readonly List<GenericAssemblyLoadContext<T>> loadContexts = new List<GenericAssemblyLoadContext<T>>();
public List<T> LoadAll(string pluginPath, string filter="*.dll", params object[] constructorArgs)
{
List<T> plugins = new List<T>();
foreach (var filePath in Directory.EnumerateFiles(pluginPath, filter, SearchOption.AllDirectories))
{
var plugin = Load(filePath, constructorArgs);
if(plugin != null)
{
plugins.Add(plugin);
}
}
return plugins;
}
private T Load(string pluginPath, params object[] constructorArgs)
{
var loadContext = new GenericAssemblyLoadContext<T>(pluginPath);
loadContexts.Add(loadContext);
var assembly = loadContext.LoadFromAssemblyPath(pluginPath);
var type = assembly.GetTypes().FirstOrDefault(t => typeof(T).IsAssignableFrom(t));
if (type == null)
{
return null;
}
return (T)Activator.CreateInstance(type, constructorArgs);
}
public void UnloadAll()
{
foreach(var loadContext in loadContexts)
{
loadContext.Unload();
}
}
}
Code language: C# (cs)
Note: This only loads one plugin type from each assembly. If you need to load multiple plugins from the same assembly, go ahead and update this to loop through assemby.GetTypes() instead of using assembly.GetTypes.GetFirstOrDefault().
Why exclude the plugin interface library?
If I were to load the plugin interface library into the assembly load context, then IsAssignableFrom() would be false even though it should be true. The way around this problem is to not load the plugin library or its dependencies into the plugin’s load context.
2 – Create a plugin
To create a plugin, you’ll first need a plugin interface. This is really just a plain old interface (nothing special about it). Define it based on what common behavior you want the plugins to implement. As mentioned above, you’ll want to define this in its own class library project. Here’s an example plugin interface:
namespace GenericPluginInterfaces
{
public interface IMessageProcessingPlugin
{
void Process(string message);
}
}
Code language: C# (cs)
Now you can create a plugin by implementing the plugin interface in a class in a different class library project. This needs to reference the plugin interface. Here’s an example:
using GenericPluginInterfaces;
public class MessageLoggingPlugin : IMessageProcessingPlugin
{
public void Process(string message)
{
Console.WriteLine($"Message Logger: {DateTime.Now:O}\tReceived message {message}");
}
}
Code language: C# (cs)
Note: I’m outputting the build to C:\MessagePluginProcessors\MessageLoggingPlugin\.
3 – Use the plugin loader
Finally, you can use the plugin loader to load the plugins and use them. There are three key steps:
- Load the plugins.
- Use them.
- Unload the plugins when they are no longer needed.
Here’s an example showing how to do these steps:
using GenericPluginInterfaces;
using System;
using System.Collections.Generic;
var pluginLoader = new GenericPluginLoader<IMessageProcessingPlugin>();
//Load all plugins from a folder
var messageProcessingPlugins = pluginLoader.LoadAll(pluginPath: @"C:\MessagePluginProcessors\");
Console.WriteLine($"Loaded {messageProcessingPlugins.Count} plugin(s)");
//Get messages from somewhere (hardcoding for simplicity here)
var messages = new List<string>()
{
"Hello World",
"Keep it simple",
"Plugins are great"
};
//Use plugins to process messages
foreach (var message in messages)
{
foreach (var plugin in messageProcessingPlugins)
{
plugin.Process(message);
}
}
//unload plugins
pluginLoader.UnloadAll();
Console.WriteLine("Unloaded plugins");
Console.ReadKey();
Code language: C# (cs)
Running this code outputs the following:
Loaded 1 plugin(s)
Message Logger: 2022-10-25T09:50:13.1233755-04:00 Received message Hello World
Message Logger: 2022-10-25T09:50:13.1284428-04:00 Received message Keep it simple
Message Logger: 2022-10-25T09:50:13.1286210-04:00 Received message Plugins are great
Unloaded plugins
Code language: plaintext (plaintext)
Comments are closed.