Plugin Development Guide¶
Plugins are a powerful way to extend Needlr's functionality and configure your application in a modular, reusable way.
Plugin Types¶
Needlr provides several main plugin interfaces, each serving a specific purpose in the application lifecycle. They are intended to be split out into different packages as necessary with some in the core set of packages and others split out into others so you can control what you incorporate.
1. IServiceCollectionPlugin¶
Configures services during the initial registration phase.
using Microsoft.Extensions.DependencyInjection;
using NexusLabs.Needlr;
public class DatabasePlugin : IServiceCollectionPlugin
{
public void Configure(ServiceCollectionPluginOptions options)
{
// Access the service collection
options.Services.AddDbContext<MyDbContext>(opts =>
opts.UseSqlServer(options.Configuration.GetConnectionString("Default")));
// Log registration activities
options.Logger.LogInformation("Database context registered");
// Access configuration
var connectionString = options.Configuration.GetConnectionString("Default");
// Register additional services
options.Services.AddScoped<IRepository, Repository>();
}
}
2. IPostBuildServiceCollectionPlugin¶
Executes after the main service collection has been built, useful for validation or late configuration.
public class ValidationPlugin : IPostBuildServiceCollectionPlugin
{
public void Configure(PostBuildServiceCollectionPluginOptions options)
{
// Validate that required services are registered
var requiredService = options.ServiceProvider.GetService<IRequiredService>();
if (requiredService == null)
{
options.Logger.LogError("IRequiredService is not registered!");
throw new InvalidOperationException("Required service missing");
}
// Perform post-build configuration
var configService = options.ServiceProvider.GetRequiredService<IConfigurationService>();
configService.Validate();
options.Logger.LogInformation("Post-build validation completed");
}
}
3. IWebApplicationBuilderPlugin¶
Configures the WebApplicationBuilder before the application is built.
public class SecurityPlugin : IWebApplicationBuilderPlugin
{
public void Configure(WebApplicationBuilderPluginOptions options)
{
// Configure services
options.Builder.Services.AddAuthentication()
.AddJwtBearer(opts =>
{
opts.Authority = options.Builder.Configuration["Auth:Authority"];
});
options.Builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
});
// Configure Kestrel
options.Builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
});
// Add configuration sources
options.Builder.Configuration.AddJsonFile("security.json", optional: true);
options.Logger.LogInformation("Security configured");
}
}
4. IWebApplicationPlugin¶
Configures the WebApplication after it's built, typically for middleware and endpoint configuration.
public class ApiPlugin : IWebApplicationPlugin
{
public void Configure(WebApplicationPluginOptions options)
{
var app = options.WebApplication;
// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthentication();
app.UseAuthorization();
// Map endpoints
app.MapGet("/api/health", () => Results.Ok(new { Status = "Healthy" }))
.WithName("HealthCheck")
.WithOpenApi();
app.MapPost("/api/data", async (DataRequest request, IDataService service) =>
{
var result = await service.ProcessAsync(request);
return Results.Ok(result);
})
.RequireAuthorization("AdminOnly");
options.Logger.LogInformation("API endpoints configured");
}
}
Plugin Discovery and Registration¶
Automatic Discovery¶
Plugins are automatically discovered through assembly scanning. You must configure a discovery strategy:
// With source generation
var webApp = new Syringe()
.UsingSourceGen()
.ForWebApplication()
.BuildWebApplication();
// With reflection
var webApp = new Syringe()
.UsingReflection()
.ForWebApplication()
.BuildWebApplication();
For built-in plugins, you do not need to annotate them with the special attributes that prevent auto-registration and injection prevention since this is done by the framework itself.
Controlling Plugin Discovery¶
Assembly Filtering¶
Control which assemblies are scanned for plugins:
var webApp = new Syringe()
.UsingSourceGen() // or .UsingReflection()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x =>
x.Contains("MyCompany.Plugins"))
.Build())
.ForWebApplication()
.BuildWebApplication();
Plugin Execution Order¶
Plugins are executed in a deterministic order based on two factors:
- Plugin Order Attribute - Explicit ordering via
[PluginOrder] - Type Name - Alphabetical sorting when orders are equal
Using the PluginOrder Attribute¶
Use the [PluginOrder] attribute to control when your plugin executes relative to others:
using NexusLabs.Needlr;
// Executes first - negative values run before default (0)
[PluginOrder(-100)]
public class DatabaseMigrationPlugin : IServiceCollectionPlugin
{
public void Configure(ServiceCollectionPluginOptions options)
{
// Run migrations before other plugins register services
options.Services.AddDbContext<MyDbContext>();
}
}
// Executes at default order (0) - no attribute needed
public class BusinessLogicPlugin : IServiceCollectionPlugin
{
public void Configure(ServiceCollectionPluginOptions options)
{
// Normal service registration
options.Services.AddScoped<IOrderService, OrderService>();
}
}
// Executes last - positive values run after default (0)
[PluginOrder(100)]
public class ValidationPlugin : IServiceCollectionPlugin
{
public void Configure(ServiceCollectionPluginOptions options)
{
// Validate all registrations are complete
}
}
Order Values¶
| Order Range | Typical Use Case |
|---|---|
| -100 to -50 | Infrastructure setup (database, logging, configuration) |
| -50 to -1 | Core services that others depend on |
| 0 (default) | Normal business logic plugins |
| 1 to 50 | Plugins that depend on other registrations |
| 50 to 100 | Validation, cleanup, and finalization |
Same-Order Determinism¶
When multiple plugins have the same order value, they are sorted alphabetically by their fully qualified type name. This ensures deterministic execution across builds:
// Both have Order = 0, so they execute alphabetically
public class AuditPlugin : IServiceCollectionPlugin { } // Executes first
public class ZipCodePlugin : IServiceCollectionPlugin { } // Executes second
Works with All Plugin Types¶
The [PluginOrder] attribute works with all Needlr plugin interfaces:
// IServiceCollectionPlugin
[PluginOrder(-50)]
public class EarlyServicePlugin : IServiceCollectionPlugin { }
// IWebApplicationBuilderPlugin
[PluginOrder(10)]
public class MiddlewareSetupPlugin : IWebApplicationBuilderPlugin { }
// IWebApplicationPlugin
[PluginOrder(100)]
public class FinalEndpointPlugin : IWebApplicationPlugin { }
// IHostApplicationBuilderPlugin
[PluginOrder(-100)]
public class HostSetupPlugin : IHostApplicationBuilderPlugin { }
Parity Between Reflection and Source Generation¶
Plugin ordering works identically whether you use reflection or source generation:
// Both produce the same plugin execution order
var reflectionApp = new Syringe()
.UsingReflection()
.ForWebApplication()
.BuildWebApplication();
var sourceGenApp = new Syringe()
.UsingSourceGen()
.ForWebApplication()
.BuildWebApplication();
Assembly Filtering¶
You can also control which assemblies are scanned for plugins:
var webApp = new Syringe()
.UsingSourceGen() // or .UsingReflection()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x => x.Contains("MyCompany.Plugins"))
.UseLibTestEntryOrdering() // Libraries → Executables → Tests
.Build())
.ForWebApplication()
.BuildWebApplication();
Execution Timeline¶
Within each lifecycle phase, plugins are sorted by [PluginOrder] then by type name:
- IServiceCollectionPlugin - During service registration
- IPostBuildServiceCollectionPlugin - After service provider is built
- IWebApplicationBuilderPlugin - Before WebApplication.Build()
- IWebApplicationPlugin - After WebApplication.Build()
Advanced Plugin Patterns¶
Configuration-Driven Plugins¶
public class FeatureTogglePlugin : IServiceCollectionPlugin
{
public void Configure(ServiceCollectionPluginOptions options)
{
var features = options.Configuration.GetSection("Features");
if (features.GetValue<bool>("EnableCache"))
{
options.Services.AddMemoryCache();
options.Services.AddSingleton<ICacheService, MemoryCacheService>();
options.Logger.LogInformation("Cache feature enabled");
}
if (features.GetValue<bool>("EnableMetrics"))
{
options.Services.AddSingleton<IMetricsService, MetricsService>();
options.Logger.LogInformation("Metrics feature enabled");
}
}
}
Composite Plugins¶
Plugins may implement multiple plugin interfaces. This is especially common for ASP.NET plugins because we may want to configure both the builder and the web application that is created.
public class MicroservicePlugin : IServiceCollectionPlugin,
IWebApplicationBuilderPlugin,
IWebApplicationPlugin
{
public void Configure(ServiceCollectionPluginOptions options)
{
// Register core services
options.Services.AddHealthChecks();
options.Services.AddHttpClient();
}
public void Configure(WebApplicationBuilderPluginOptions options)
{
// Configure distributed tracing
options.Builder.Services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation());
}
public void Configure(WebApplicationPluginOptions options)
{
// Configure middleware and endpoints
options.WebApplication.UseHealthChecks("/health");
options.WebApplication.MapMetrics();
}
}
Plugin with Dependencies¶
The built-in plugins do not support dependency injection through their constructors, so if you need dependencies then you will need to access them from the dependency injection framework based on the lifecycle.
public class DependentPlugin : IPostBuildServiceCollectionPlugin
{
public void Configure(PostBuildServiceCollectionPluginOptions options)
{
// Get services that were registered by other plugins
var dbContext = options.ServiceProvider.GetRequiredService<MyDbContext>();
var cache = options.ServiceProvider.GetService<IMemoryCache>();
if (cache != null)
{
// Initialize cache with data from database
var initialData = dbContext.Settings.ToList();
foreach (var setting in initialData)
{
cache.Set(setting.Key, setting.Value);
}
}
}
}