Lightweight, test-driven plugin framework for .NET 9 (C# 13) with first-class ASP.NET Core integration. Build features as independently versioned plugins, discover and load them via configuration, and wire them into your app through a small, well-defined lifecycle.
— Core contracts live in src/lowlandtech.plugins
— ASP.NET Core integration in src/LowlandTech.Plugins.AspNetCore
— Samples in samples/
— Tests in tests/
Supported runtime/tooling
- .NET 9 (C# 13)
Modern apps need to ship features quickly without turning into tangled monoliths. Teams want to:
- Add or remove capabilities at runtime without invasive code changes.
- Keep dependency registration, async initialization, and host wiring predictable.
- Discover plugins from configuration and load them reliably across environments.
- Validate plugin identity and avoid conflicts between modules.
Doing this consistently with DI, configuration, and host integration is error-prone without a clear contract and lifecycle.
LowlandTech Plugins provides:
- Simple contract:
IPluginand a basePlugintype with a three-phase lifecycle:Install,ConfigureContext,Configure. - Clean integration:
IServiceCollection.AddPlugins()andWebApplication.UsePlugins()for ASP.NET Core; Lamar support is available in the core package. - Config-driven discovery: Reads a
Pluginssection from configuration, attempts assembly-by-name or file discovery, and instantiatesIPlugintypes. - Safety and consistency: Guarded plugin IDs via
[PluginId]attribute; duplicate IDs are rejected. - Proven by tests: Comprehensive specs under
tests/for discovery, validation, lifecycle, error handling, and Lamar usage.
- Define a plugin:
// samples/lowlandtech.sample.backend/BackendPlugin.cs
using LowlandTech.Plugins;
using LowlandTech.Plugins.Types;
[PluginId("306b92e3-2db6-45fb-99ee-9c63b090f3fc")]
public class BackendPlugin : Plugin
{
public override void Install(IServiceCollection services)
{
services.AddSingleton<BackendActivity>();
}
public override Task Configure(IServiceProvider container, object? host = null)
{
if (host is WebApplication app)
{
app.MapGet("/weatherforecast", () => new[] { "Sunny", "Cloudy" });
}
return Task.CompletedTask;
}
}- Configure plugins (optional when adding explicitly):
// appsettings.json
{
"Plugins": [
{ "Name": "LowlandTech.Sample.Backend", "IsActive": true }
]
}- Wire up in ASP.NET Core:
// samples/lowlandtech.sample.api/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register by type (explicit)
builder.Services.AddPlugin<BackendPlugin>();
// Or discover from configuration (appsettings.json -> Plugins)
builder.Services.AddPlugins();
var app = builder.Build();
// Run plugin Configure(...) against the host
app.UsePlugins();
app.Run();Run it:
dotnet builddotnet run --project samples/lowlandtech.sample.api
- Projects
- Core:
src/lowlandtech.plugins—IPlugin,Plugin, options, guards, and Lamar-focused helpers (ServiceRegistry,IContainer). - ASP.NET Core:
src/LowlandTech.Plugins.AspNetCore—IServiceCollection.AddPlugins(),IServiceCollection.AddPlugin<T>(), andWebApplication.UsePlugins().
- Core:
- Lifecycle (from
src/lowlandtech.plugins/IPlugin.cs)Install(IServiceCollection)— register services.ConfigureContext(IServiceCollection)— async context setup (optional).Configure(IServiceProvider, object? host)— finalize wiring;hostcan beWebApplication.
- Discovery (
src/LowlandTech.Plugins.AspNetCore/Extensions/PluginExtensions.cs)- Reads
Pluginsfrom configuration intoPluginConfig(Name,IsActive). - Attempts assembly resolution by name, then searches candidate paths; creates the first concrete
IPlugintype found. - Assigns
plugin.Nameandplugin.IsActive; logs progress; falls back to DLL scan if needed.
- Reads
- Registration helpers
- ASP.NET Core:
services.AddPlugin(new MyPlugin()),services.AddPlugin<MyPlugin>(),services.AddPlugins();app.UsePlugins()to invokeConfigure(...). - Lamar (core):
ServiceRegistry.AddPlugin(...),ServiceRegistry.AddPlugins(),IContainer.UsePlugins(...)for scenarios using Lamar.
- ASP.NET Core:
- Identity and validation
[PluginId("<guid>")]on the plugin type provides a stable ID (src/lowlandtech.plugins/Types/PluginId.cs).Guard.Against.MissingPluginId(...)enforces presence and rejects duplicates when adding plugins.
- Configuration shape
Plugins: [ { "Name": "<AssemblyOrNamespace>", "IsActive": true } ].- Nested
Plugins:Pluginsis tolerated for compatibility in tests.
- Samples
- API host:
samples/lowlandtech.sample.api(usesAddPlugins+UsePlugins). - Backend plugin:
samples/lowlandtech.sample.backend(example plugin with[PluginId]).
- API host:
- Build:
dotnet build - Test:
dotnet test(or filter, e.g.,--filter "VCHIP-0010-UC04") - Run sample API:
dotnet run --project samples/lowlandtech.sample.api
This repository includes build.ps1, a helper to bump versions, build, and pack projects to a local folder for consumption by other local projects.
Basic usage (pack core projects to C:\Workspaces\Packages):
-
Pack both main libraries and write packages to the local folder:
.�uild.ps1 -ProjectFile all -OutputPath 'C:\Workspaces\Packages' -
Pack a single project (e.g. AspNetCore integration):
.�uild.ps1 -ProjectFile 'src\LowlandTech.Plugins.AspNetCore\LowlandTech.Plugins.AspNetCore.csproj' -
Increment the minor version while packing:
.�uild.ps1 -VersionIncrement Minor
Notes
- The script updates
Version/PackageVersionin the specified .csproj files. Commit the changes if you want them recorded in git. - The default output path is
C:\Workspaces\Packages; clear that folder before running the script if you want only the freshly-produced packages:Remove-Item 'C:\Workspaces\Packages\*' -Force - If you prefer not to produce symbol packages (
.snupkg), either removeIncludeSymbols/SymbolPackageFormatfrom the project file(s) or adjust the pack step to suppress symbols.
This section provides a quick way to publish local NuGet packages for downstream development and CI validation.
- Follow the test-first style in
tests/and keep changes small. - Use
@VCHIP-xxxxannotations in tests for traceability when applicable.
See LICENSE for details.
Repo: https://github.com/lowlandtech/lowlandtech.plugins