#2 Plugin-Based Web App in Dotnet - Loading Plugins
THIS IS A WORK IN PROGRESS ARTICLE! I AM ACTIVELY EDITING IT RIGHT NOW!⌗
there are LOTS of typos as I have not checked this text in grammarly yet…
Chapters⌗
Writing those takes time. Expect to see one published per one-two weeks.
-
Loading plugins
-
PluginBase, IPlugin -
Creating plugin, DependencyInjection -
Controllers, Views -
Hooks and Triggers - better event system -
Advanced: Unit tests, unloading plugins
Dynamic loading⌗
C# being a compiled language requires to dynamically load compiled assemblies in memory using reflection in order to modify the application. In this part I will tell you about .net’s built-in methods to do this. Along with achieving the needed result: assemblies being loaded, - we’ll solve problems like findng plugin’s main .dll
, dependency resolving and avoiding dependency version conflicts.
What do we load? (Plugin folder, Manifest.json)⌗
First of all, we need to determine where do we look for the .dll
files that we want to load. My solution is simple: We will create a folder /Plugins
in the app’s current directory and store plugin’s files in folders with this structure:
Plugins
- ExamplePlugin
- - ExamplePlugin.dll
- - ExamplePlugin.deps.json
- - ExamplePluginDependency.dll
- - manifest.json
- - Assets/...
- NicePlugin
- - NicePlugin.dll
- - NicePlugin.deps.json
- - manifest.json
Now we need a way to determine: which .dll
file is the main plugin file. I decided to create a manifest.json
file - a file that contains plugin’s metadata: name, version, author and main .dll
file name.
Our first step would be to load and parse all manifest.json
files, then we load main .dll
files, referenced in manifests.
{
"id": "example.plugin",
"version": "1.23.4:5678",
"name": "Example Plugin",
"assembly": "EamplePlugin.dll"
}
AssemblyLoadContext⌗
image placeholder: assemblyLoadContextDiagram.svg
The next question is how are we going to load those .dll
files, that we got from manifest.json
files?
AssemblyLoadContext
is used to isolate groups of loaded assemblies from each other to resolve version conflicts. Let’s say that PluginA
references CoolLibrary.Abstractions v.3.1
, while PluginB is referencing the same CoolLibrary.Abstractions
, but v.2.5.
In order for this to work, we will create an AssemblyLoadContext for each main .dll
plugin and load it’s dependencies from this context.
Oh right! Plugins have dependencies, but AssemblyLoadContext
WOULD NOT find those automagically. We will need to explicitly tell it to look for them. In order to do so, we will create a PluginLoadContext : AssemblyLoadContext
. This class will use an AssemblyDependencyResolver
class that will read .deps.json
file of a provided file and resolve dependencies from it.
.deps.json is created automatically when you compile any csharp project and includes all your dependencies!
public class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base()
{
// initialize the dependency resolver
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
// use the dependency resolver to find all depended libraries and load them
string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
// if successful, load the main assembly
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null!;
}
// essentially, dark magic. don't think about it 😛
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
Project template for the plugin file⌗
This part is more about what we will need in the future. As our main application uses the MVC
pattern, our plugins would have views and controllers. We should modify our .csproj
as follows:
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>
<Reference Include="Prototype.ModularMVC.PluginBase">
<Private>false</Private>
<HintPath>..\Prototype.ModularMVC.App\Prototype.ModularMVC.PluginBase\bin\Debug\net8.0\Prototype.ModularMVC.PluginBase.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App">
</FrameworkReference>
</ItemGroup>
<ItemGroup>
<Content Update="manifest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Explanation:
Microsoft.NET.Sdk.Razor
will make sure that our.cshtml
razor pages will be compiled to classes and added to the output assembly.EnableDynamicLoading
hints that this library is intended to be loaded dynamically, rather than executed or referenced directly.Microsoft.AspNetCore.App
framework reference will allow us to use ASP.NET-specific classes.AddRazorSupportForMvc
speaks for itself, doesn’t it?manifest.json
should be created and filled by the author. Then copied to the output directory.- Referencing
PluginBase
as well as thePluginBase
library itself will be explained in the next chapter.
Conclusion⌗
At this point, we can locate and load our plugins with their dependencies at runtime.
This article is based on this microsoft learn article, as well as my code base. I have added some abstractions to make it easier to extend my loading mechanism, so the original article might be a little easier to understand. I didn’t simplified my examples because I think that anybody aiming to create the same plugin system as I did will eventually reach similar conclusions and will make similar modifications to the code from the original article.
- Program.cs - using manifest loader and plugin loader to load plugins
- ManifestLoader.cs - loads manifests
- ManifestBasedPluginLoader.cs - loads plugins, using paths from the
ManifestLoader
. - PluginLoadContext.cs - Implementation of an
AssemblyLoadContext.cs
that resolves dependencies.
In the next article, we will talk about communication between the server and loaded plugins, PluginBase
library, and using dependency injection to tie everything together.