209 lines
9.3 KiB
Markdown
209 lines
9.3 KiB
Markdown
+++
|
|
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 = true
|
|
+++
|
|
|
|
<script src="/js/repo-card.js"></script>
|
|
<div class="repo-card" data-repo="the1mason/Prototype.ModularMVC" data-theme="dark-theme"></div>
|
|
|
|
# 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.
|
|
|
|
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" >}}
|
|
|
|
<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>
|
|
|
|
{{< /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.
|
|
|
|
|