Author:
C# implementation⚓︎
The C# implementation of a plugin system is closely tied to the .NET runtime.
In practice, a plugin is usually a compiled assembly (.dll), and the host uses reflection to discover types that implement a shared contract.
Core idea⚓︎
The usual implementation has three parts:
- Assembly loading: load the plugin assembly from disk.
- Interface constraint: require plugins to implement a shared interface such as
IPlugin. - Reflection-based instantiation: find a compatible type and create an instance at runtime.
.NET-specific implementation⚓︎
Here are some .NET-specific APIs commonly used in a C# plugin system:
- Plugin loading:
AssemblyLoadContext.LoadFromAssemblyName(...)loads the plugin assembly into an isolated load context.1 - Dependency resolution:
AssemblyDependencyResolverhelps resolve the plugin's dependencies.2 - Reflection:
Assembly.GetTypes()is part of .NET reflection and is used to discover types that implement the shared contract.3 - Dynamic instance creation:
Activator.CreateInstance()creates the plugin instance dynamically.4
This is where reflection appears in practice: the host inspects types from the loaded assembly and then instantiates a compatible plugin type dynamically.
Minimal example⚓︎
The following simplified version is adapted from the Microsoft tutorial, but split into smaller parts for readability.
1. Shared contract⚓︎
2. Host side: load plugin and create instance⚓︎
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
class Program
{
static void Main()
{
var assembly = LoadPlugin("plugins/HelloPlugin.dll");
var plugin = CreatePlugin(assembly);
plugin.Execute();
}
static Assembly LoadPlugin(string pluginPath)
{
var loadContext = new PluginLoadContext(pluginPath);
var assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(pluginPath));
return loadContext.LoadFromAssemblyName(assemblyName);
}
static IPlugin CreatePlugin(Assembly assembly)
{
var pluginType = assembly
.GetTypes()
.First(type => typeof(IPlugin).IsAssignableFrom(type)
&& !type.IsInterface
&& !type.IsAbstract);
return (IPlugin)Activator.CreateInstance(pluginType)!;
}
}
3. Isolated load context⚓︎
using System.Reflection;
using System.Runtime.Loader;
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath is null ? null : LoadFromAssemblyPath(assemblyPath);
}
}
Flow⚓︎
- The host loads a
.dllplugin assembly through a custom load context. - The runtime resolves the plugin's dependencies.
- The host scans types in the assembly.
- The host finds a class that implements
IPlugin. - The host creates the instance dynamically.
- The host calls the plugin through the interface.
Notes⚓︎
- In modern .NET,
AssemblyLoadContextis often preferred when you need better isolation or unloading support. - The Microsoft tutorial uses this split structure because it scales better than a single
Assembly.LoadFrom(...)snippet. - In production systems, plugin discovery usually also includes metadata validation, version checks, and error handling.
LoadFromAssemblyName(...)starts loading the main plugin assembly into the custom load context. When additional dependencies are needed, the runtime may call the overriddenLoad(...)method in that context.