Introduction
In today’s article, we will take a look at using the plugin pattern in C#. The plugin pattern allows us to drop assemblies into a particular location and use their implementation in our applications. This is one way we can create loosely coupled applications. Let us begin.
Creating the Interface and Implementations
We will start by creating a C# console application in Visual Studio 2022 community edition as below,
Next, we add a class library that will host the interface that will be implemented by various plugin assemblies.
Rename Class1.cs to implementation.cs
Replace the code in this file to the below,
namespace ImplementationBase;
public interface ITask {
string Id {
get;
}
string Description {
get;
}
int Run();
}
Next, we will create two assemblies (C# class library projects) with classes that will implement this interface. We will name these ImplementationOne and ImplementationTwo. We will name the classes in them as TaskOne and TaskTwo. I will skip the details on creating these two projects and classes as we follow the same procedure as above.
The code for the two classes is as below,
using ImplementationBase;
namespace ImplementationOne;
public class TaskOne: ITask {
public string Id {
get;
} = "One";
public string Description {
get;
} = "First Implementation plugin";
public int Run() {
return 1;
}
}
using ImplementationBase;
namespace ImplementationTwo;
public class TaskTwo: ITask {
public string Id {
get;
} = "Two";
public string Description {
get;
} = "Second Implementation plugin";
public int Run() {
return 2;
}
}
Also, add them below in the project files between the project tags.
<ItemGroup>
<ProjectReference Include="..\ImplementationBase\ImplementationBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
This is very important as it directs the project to reference the base implementation and not the local copy. Without these entries, the plugin pattern will not work.
Creating the main implementation
In the main project, add a new file called “PluginLoadContext.cs” and paste the below code in it,
using System.Reflection;
using System.Runtime.Loader;
namespace PluginExample {
class PluginLoadContext: AssemblyLoadContext {
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) {
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName) {
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;
}
}
}
Next, add a reference to the ImplementationBase project. Finally, update the code in the Program.cs file as below,
using ImplementationBase;
using PluginExample;
using System.Reflection;
class Program {
static void Main(string[] args) {
try {
var loadLocations = new string[] {
@ "C:\Temp\Plugins\1\ImplementationOne.dll",
@ "C:\Temp\Plugins\2\ImplementationTwo.dll"
};
IEnumerable < ITask > tasks = loadLocations.SelectMany(pluginPath => {
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
foreach(ITask task in tasks) {
Console.WriteLine($ "{task.Id}\t - {task.Description}");
Console.WriteLine($ " The running task returns - {task.Run()}");
}
} catch (Exception ex) {
Console.WriteLine(ex);
}
}
static Assembly LoadPlugin(string relativePath) {
var pluginLocation = relativePath;
var loadContext = new PluginLoadContext(pluginLocation);
return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
}
static IEnumerable < ITask > CreateCommands(Assembly assembly) {
int count = 0;
foreach(Type type in assembly.GetTypes()) {
if (typeof(ITask).IsAssignableFrom(type)) {
ITask ? result = Activator.CreateInstance(type) as ITask;
if (result != null) {
count++;
yield
return result;
}
}
}
}
}
Running the solution
Now, we compile the solution and copy the output from the ImplementationOne and ImplementationTwo projects to the following locations:
C:\Temp\Plugins\1\ <ImplementationOne output code>
C:\Temp\Plugins\2\ <ImplementationTwo output code>
We now run the main application and see the below output,
Summary
In this article, we took a look at creating the plugin pattern in C#. We saw how to create implementations of an interface and how these can be loaded dynamically. In the future, if we added another implementation, we could simply add it to the list and process it as well. Happy coding!