Announcing Prise, a plugin framework for .NET Core
📣 Prise [/Prease/] is a versatile and highly customizable plugin framework for .NET Core applications, written in .NET Core.
Prise is a plugin framework for .NET Core applications, written in .NET Core. The goal of Prise, is enable you to write…
For those who are wondering where the name Prise comes from, pries is a bastardized word in Flemish that means ‘the wall socket’: http://www.vlaamswoordenboek.be/definities/geschiedenis/378
Who do we need (another) plugin framework ?
Over the past years, I’ve worked in various environments where a plugin framework would have been beneficial to the overall structure of the software. For example. In the industry of Payroll, the calculation of net wages for employees can be horrid😱. Not only does the Belgian law makes this difficult, but also, the wage is calculated in four tiers. Wage, Brute, Taxable and Net.
Each tier has its own set of rules, that, depending on the context, produce a different result. In this kind of calculation, it is advised to separate these calculations out into small, testable components and inject them when needed, based on the context of the calculation.
Does this sound familiar to you? Then you might just benefit by using a plugin framework.
Another example would be a large desktop application that loads different UI components based on whatever permissions the user has. If a component can’t be loaded, the rest of the application could still continue to function as normal, until this component is fixed.
Again, we see that the loading of components is context-driven, the context being; the permissions of the user.
Calculation of car insurance and house insurance all depend on the context: the state of the house, the cost of the purchase, the income of the person requesting the insurance, and so on. When the context changes, different components need to come into play and change the outcome of the premium that needs to be payed.
If you’d write a traditional monolithic application, you’d be changing calculation rules on a weekly basis. Separating this out into components that you can hot-load and plugin on demand, is key to the maintainability and stability of the application.
If you’re writing an integration platform that interacts with data from different third party providers, you don’t want to highly couple these providers into your system. Each provider will have it’s own plugin that you load at runtime in order to connect to that provider when it is required, based on the context.
I believe, in this microservice world, a decent plugin system can help us keep focus on what really matters, writing good quality software.
With the footnote that you write tests for your plugins, obviously😁
Prise 1.0.0 supports the following features straight out of the box:
- Easy initial setup
- Eager loading of a plugin
- Lazy loading of a plugin
- Loading of one plugin from one assembly
- Loading of many plugins from one assembly
- Loading of plugins from disk
- Loading of plugins from network
- Loading of plugins with their own dependencies (via dependency injection)
- Supporting older plugins that no longer abide to the current contract
Going forward, I hope to improve Prise in the following ways:
- Implementing assembly discovery
- Stringly typed plugins, instead of Contract typed
- Loading of many plugins from many assemblies
- Add resiliency when loading assemblies from the network
- Loading serialized assemblies from a database
- Creating inline, in-memory plugins (without loading an assembly)
- Implementing assembly (or plugin) caching
How does Prise work?
At the heart, Prise uses a DispatchProxy of your contract. Every call to a method channels through this proxy and is delegated to the loaded plugin.
After the plugin is loaded, a DispatchProxy is created, when a call to your contract is made, the proxy finds the most suitable corresponding method inside the plugin and invokes it with the provided parameters.
If your contract expands, whilst the plugin still provides a valid candidate method for the calling method, it will continue to work. There is no need to retroactively upgrade your old plugins in order to support the new system going forward.
To start off using Prise, we’ll consider the following example project setup:
- A class library Contract project
- Our first class library HelloWorldPlugin project
- The MyHost host application that will make use of Prise
The Contract project is a plain old classlib project (netstandard2.0).
dotnet new classlib
This project does not contain any relation to Prise, it just contains one interface (the contract):
The HelloWorldPlugin project is also a plain old classlib project but it requires a reference to Prise.Infrastructure in order to register the plugin. It also needs to reference our contract.
dotnet new classlibdotnet add package Prise.Infrastructuredotnet add reference ../Contract/Contract.csproj
We’re now going to write our first Prise plugin. A HelloWorldPlugin.
In order for Prise to discover this plugin, we’ll need to annotate this with a Plugin Attribute and specify the contract that the plugin is implementing.
Implementing the interface is optional, as long as you expose the SayHello method publicly using one string input parameter and a string return type. Prise will be able to call this plugin.
But to keep it simple, we’ll just implement the IHelloWorldPlugin interface.
We’re now ready to publish this plugin.
The published plugin can be found in the bin/Debug/netstandard2.0/publish directory.
This directory contains all of the runtime dependencies for our plugin. The contract, the Prise Infrastructure and the plugin itself, off course.
Later on, we’ll copy the contents of this directory into our host application.
The Host project is an ASP.NET Core 3.0 project. It needs to reference Prise and the Contract project.
dotnet new webapidotnet add package Prisedotnet add reference ../Contract/Contract.csproj
You can ignore the Weather stuff.
Prise will look for a Plugins directory by default and load the plugin from this directory at runtime. So we’ll need to create a Plugins directory when the application is deployed or launched.
Now we need to hookup Prise in the Startup.cs file, this is done via adding the following one-liner to the ConfigureServices method:
services.AddPrise<IHelloWorldPlugin>(options => options.WithPluginAssemblyName("HelloWorldPlugin.dll"));
Don’t forget the namespace imports in the Startup.cs file:
using Contract;using Prise.Infrastructure.NetCore;
Now, let’s wrap it up by creating a HelloController to say hello via our plugin!
Notice that the HelloController does not require a reference to Prise. The IHelloWorldPlugin is registered in the .NET Core dependency injection container using the IHelloWorldPlugin interface. And it can be resolved as such.
This is called Eager plugin loading, you could also inject a IPluginLoader and load the plugin whenever you find it needed, that would be Lazy plugin loading.
Now run the MyHost application using dotnet run.
Navigate to the application URL using a query parameter called input to see the result:
🤔 🤨 😐
It fails to find the HelloWorldPlugin assembly, because we forgot to copy that into the Plugins directory 😅
No worries, let’s just use the power of Prise and hot-swap this assembly into the running application! 😈
Copy the contents from the bin/Debug/netstandard2.0/publish directory from our HelloWorldPlugin into the bin/Debug/netcoreapp3.0/Plugins directory of the MyHost application.
What’s next ?
Please check out the example here:
This project contains a simple plugin HelloWorldPlugin. The host is an ASP.NET Core web api that loads the plugin from…
There’s also a more complex setup using Complex Plugins with Dependency Injection, Lazy Loading, custom plugin loading, loading multiple plugins, …
Check that out here: