+++ title = "#2 Plugin-Based Web App in Dotnet - Loading Plugins" date = "2024-02-4T00:00:00+00:00" author = "the1mason" authorTwitter = "the0mason" #do not include @ tags = ["dotnet", "web", "prototype"] keywords = ["prototype", "dotnet", "guide", "plugins", "plugin-based", "web application", "ASP.NET", "C#", ".NET 8", "Programming", "Software Architecture"] description = "The second part of my journey to create a modular based web app. In this part I'll tell you how do you load plugins during runtime." showFullContent = false readingTime = true hideComments = false draft = false +++
# THIS IS A WORK IN PROGRESS ARTICLE! I AM ACTIVELU EDITING IT RIGHT NOW! # Chapters Writing those takes time. Expect to see one published per one-two weeks. 1. [Idea, Stack](/posts/modular-app-1) 2. Loading plugins 3. ~~PluginBase, IPlugin~~ 4. ~~Creating plugin, DependencyInjection~~ 5. ~~Controllers, Views~~ 6. ~~Hooks and Triggers - better event system~~ 7. ~~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: {{< code language="txt" title="Example directory tree" id="1" expand="Show" collapse="Hide" isCollapsed="false" >}} Plugins - ExamplePlugin - - ExamplePlugin.dll - - ExamplePlugin.deps.json - - ExamplePluginDependency.dll - - manifest.json - - Assets/... - NicePlugin - - NicePlugin.dll - - NicePlugin.deps.json - - manifest.json {{< /code >}} 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. {{< code language="json" title="manifest.json" id="1" expand="Show" collapse="Hide" isCollapsed="false" >}} { "id": "example.plugin", "version": "1.23.4:5678", "name": "Example Plugin", "assembly": "EamplePlugin.dll" } {{< /code >}} [ManifestLoader.cs](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.PluginBase/Impl/ManifestLoaders/ManifestLoader.cs) [Program.cs](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.App.Server/Program.cs) --- # 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!* {{< code language="csharp" title="PluginLoadContext" id="2" expand="Show" collapse="Hide" isCollapsed="false" >}} 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; } } {{< /code >}} [PluginLoadContext.cs](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.PluginBase/PluginLoadContext.cs) --- # 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: {{< code language="xml" title="ExamplePlugin.cshtml" id="3" expand="Show" collapse="Hide" isCollapsed="false" >}} net8.0 enable enable true true false ..\Prototype.ModularMVC.App\Prototype.ModularMVC.PluginBase\bin\Debug\net8.0\Prototype.ModularMVC.PluginBase.dll Always {{< /code >}} 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 the `PluginBase` 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](https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support), 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](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.App.Server/Program.cs) - using manifest loader and plugin loader to load plugins - [ManifestLoader.cs](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.PluginBase/Impl/ManifestLoaders/ManifestLoader.cs) - loads manifests - [ManifestBasedPluginLoader.cs](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.PluginBase/Impl/PluginLoaders/ManifestBasedPluginLoader.cs) - loads plugins, using paths from the `ManifestLoader`. - [PluginLoadContext.cs](https://github.com/the1mason/Prototype.ModularMVC/blob/869cebee5e55cb3e8bf8f228d712bbda621d6b17/Prototype.ModularMVC.App/Prototype.ModularMVC.PluginBase/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.