Puzzle is a simple plugin system for .NET. It does not aim to be a fancy, best-to-ever-do-it package. It is for projects where simple, basic plugin support is needed.
Plugins are loaded in isolation; that is, they are loaded in their own AssemblyLoadContext
and are isolated from the rest of the application services. Plugin services are injected alongside your application services so that you can resolve them from your DI container without any extra work. However, a plugin service will not be able to resolve your application services.
Download from NuGet.
This is the main package which should be used by plugin hosts. If you are writing a plugin, you will want the Puzzle.Abstractions
package.
Adding plugin support to your application is as easy as one line:
...
builder.Services.AddPlugins(builder.Configuration);
...
Alternatively, for Puzzle to automatically pass itself the configuration:
...
builder.AddPlugins();
...
AddPlugins()
should be called as the last step before calling builder.Build()
.
Puzzle configuration options should be nested under a Plugins
section.
{
"Plugins": {
"Locations": [
"/path/to/plugins"
]
},
"StartupThreshold": "00:00:01.0000"
}
The locations in which Puzzle will look for plugins. Each plugin must be nested one level deep inside its own folder. For example:
.
├── ...
├── plugins # The directory which you will include in `Locations`
│ ├── com.company.pluginA # The sub-directory for a plugin
| | |── PluginA.dll
| | └── ... # Other plugin files
│ ├── com.company.pluginB # The sub-directory for another plugin
| | |── PluginB.dll
| | └── ... # Other plugin files
└── ...
A target time which plugin initialization should not exceed during application startup. If plugin initialization exceeds this time, a warning will be logged.
Download from NuGet.
If you are creating a plugin, this is the package for you.
For a plugin service to be registered you must mark it with the Service<>
attribute.
The Service<>
attribute takes a Lifetime
argument. This can be Scoped
, Singleton
, or Transient
.
NOTE: This should only be used for services which are implementing an abstraction for the host app. Let's suppose that you have the following setup:
.
├── ...
├── PluginHostApp
| └── ...
├── PluginAbstractions
│ ├── IDataSource.cs
| └── ...
├── SqlitePlugin
│ ├── Abstractions
│ | ├── IConnectionFactory.cs
| | └── ...
│ ├── SqliteConnectionFactory.cs
│ ├── SqliteDataSource.cs
| └── ...
└── ...
PluginHostApp
is going to be looking for implementations of IDataSource
, which your plugin is going to provide via SqliteDataSource
. Because you want SqliteDataSource
to be picked up by Puzzle, you must mark it with the Service<IDataSource>
attribute. Suppose that SqliteDataSource
has a dependency on IConnectionFactory
, which is implemented by SqliteConnectionFactory
. You would not mark SqliteConnectionFactory
with the Service<IConnectionFactory>
attribute because PluginHostApp
does not need to know about the IConnectionFactory
. Instead, you should bootstrap IConnectionFactory -> SqliteConnectionFactory
as detailed below.
Your service must be public
for Puzzle to pick it up.
Your plugin must export an implementation of IPluginMetadata
. If you do not export an implementation, your plugin will not be loaded.
public sealed class MyPluginMetadata : IPluginMetadata
{
public string Id => "com.company.plugin";
public string Name => "My Plugin";
}
If your plugin requires services to operate you can inject them via an exported IPluginBootstrapper
implementation. Only one implementation is supported per plugin.
Puzzle will automatically add logging and IHttpContextAccessor
for you; you should not bootstrap them yourself.
public sealed class MyPluginBootstrapper : IPluginBootstrapper
{
public IServiceCollection Bootstrap(IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<MyConnectionBuilder>(configuration.GetRequiredSection("Connections"));
return services;
}
}
The IConfiguration
object which is passed in has two sources:
- Environment variables
- (Optional) A
settings.json
file located at the root of your plugin.
Download from NuGet.
If you want to support Blazor components in your application host you will need to install Puzzle.Blazor
.
...
builder.AddPlugins(config => config.AddBlazor());
...
app.MapRazorComponents<App>()
.AddPluginComponents(app)
...;
...
Puzzle will automatically register all exported IComponent
instances from plugins. When components from these plugins are used, Puzzle will activate the components using the plugin's DI container instead of the host application's DI container.
In order for DI to work the component must use constructor injection; NOT property injection via @inject
. The notable exceptions to this rule are NavigationManager
and IJsRuntime
.