Adding 2nd part

This commit is contained in:
the1mason 2024-02-05 04:12:56 -08:00
parent 0fd5396fbd
commit a0c71b0055
4 changed files with 227 additions and 73 deletions

View File

@ -22,7 +22,7 @@ Writing those takes time. Expect to see one published per one-two weeks.
1. Idea, Stack
2. ~~Loading plugins~~
2. [Loading plugins](/posts/modular-app-2)
3. ~~PluginBase, IPlugin~~
@ -83,7 +83,7 @@ Blazor WASM does not support dynamic loading for plugins. Because of that, plugi
---
# Resoliton
# Conclusion
In the end, this will be an ASP.NET MVC web application with HTMX library for the client.

View File

@ -0,0 +1,206 @@
+++
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
+++
<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 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" >}}
<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.

View File

@ -1,71 +0,0 @@
+++
title = "Plugin-Based Web Application in Dotnet"
date = "2024-01-20T00:00:00+00:00"
author = "the1mason"
authorTwitter = "the0mason" #do not include @
cover = "posts/modular-app/title.svg"
tags = ["dotnet", "web", "prototype"]
keywords = ["prototype", "dotnet", "guide", "plugins", "plugin-based", "web application", "ASP.NET", "C#", ".NET 8", "Programming", "Software Architecture"]
description = "Have you ever thought about making a web application, that could be easily extended by third-party developers? I've been thinking about making this app for a while, so here's my experience..."
showFullContent = false
readingTime = true
hideComments = false
draft = true
+++
### Table of Contents
- [Introduction](#introduction)
- [Why](#why)
- [How](#how)
- [The Prototype](#the-prototype)
- - [IPlugin](#iplugin)
- - [Loading Plugins](#loading-plugins)
- - [Hooks and Triggers](#hooks-and-triggers)
- [Sources](#sources)
# Introduction
This post is about my experience of making a prototype of a web app with plugin support as well as my reasoning. As a result, this app could be extended by adding compiled `.dll` class libraries into a plugins folder. I've made it possible to load not only classes but also views and API controllers. You can check out the final version of this prototype in this [GitHub Repo](//github.com/the1mason/prototype.modularmvc).
Also right now I'm building a web application, using similar concepts. As of now, it's not on github, but you can find it [here](//git.the1mason.com/the1mason/octocore).
<script src="/js/repo-card.js"></script>
<div class="repo-card" data-repo="the1mason/Prototype.ModularMVC" data-theme="dark-theme"></div>
# Why
Self-hosted web applications can solve different problems and be of use to a variety of different people with slightly different needs. For this to work, I think that such an application should provide an option to extend its functionality. This would allow other people to build an ecosystem of different extensions.
# Stack
![C#, MVC, HTMX](/posts/modular-app/stack.svg)
Making a C# web application was my goal from the begginning. That's my backend language for this project, but why MVC and what is HTMX?
Let's have a quick look at worthy alternatives, and then I'll explain my choices.
`Blazor WASM` does not support runtime assembly loading, which makes client extension impossible. It has [Lazy loading](https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-lazy-load-assemblies?view=aspnetcore-8.0), but it still requires these assemblies to be defined in the project file, which is not viable for our case.
`WebApi + <name a JS framework>` is also not an option. It would require plugins to be written in two languages. Also, the client would have to be rebuilt after each plugin installation.
So, what are MVC and HTMX?
`ASP.NET MVC` is an older framework that uses `Razor Pages` to render HTML on a server and return it to the client. But it has a significant problem: Lack of reactivity. Each user's action would have to be processed on the server like in the good old days...
So in order for the app to be usable, I have decided to go with `HTMX`:
> htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
> *— from [htmx.org](htmx.org)*
<div style="display: flex; justify-content: center">
<img src="/posts/modular-app/createdwith.jpeg" style="height: 50px;"/>
</div>
<br>
This means, that I will use razor pages to generate an HTML body with HTMX tags, and return it to the client. The client would then read HTML, executing HTMX tags. Ain't that awesome?
# How
[This](https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support) article gives the base for plugin loading. It doesn't cover the ASP.NET part but otherwise is very helpful.
Still, I will explain basic concepts here. Just to have everything in one place.

View File

@ -0,0 +1,19 @@
+++
title = "鈴々"
date = "2024-01-20T00:00:00+00:00"
author = "the1mason"
authorTwitter = "the0mason" #do not include @
tags = ["sh*tpost"]
keywords = []
showFullContent = false
readingTime = true
hideComments = false
draft = false
+++
鈴々 is rinrin
yeah...
idk what to say...
have you heard that I'm an [HTMX CEO](//htmx.ceo) now? :p