C# – How to implement the plugin pattern

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).

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.
Plugin architecture diagram with generic plugin loader

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 and returns them.

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)

2 thoughts on “C# – How to implement the plugin pattern”

  1. This was a nice tutorial. Thank you.

    However, there are a couple of things.

    First, it would be really nice for you to list the packages that you are including. For instance, System.Reflection and System.Runtime.Loader.

    Second, in your main program you have the following line:

    List messages = GetMessages();

    I have looked and looked and I cannot find where GetMessages() is ever defined. I played around I managed to get it to work by adding it, but you should add this to your tutorial.

    Reply
    • Hi Mike,

      I’m glad this helped, and thank you for the good suggestions. I’ll work on cleaning this up a bit.

      Regarding GetMessages(), I often put placeholders in example code to not distract from the main topic. It’d probably be better to hardcode the list of messages here + add a comment to make it clear this is just a placeholder for “get your data somehow.”

      I appreciate the helpful feedback!

      Reply

Leave a Comment