🔌 Plugin Development Guide
Complete guide to creating, installing, and configuring plugins for BlazorBlueprint
🎯 Overview
BlazorBlueprint uses a modular plugin system that allows you to extend functionality without modifying core code. Plugins are self-contained, independently versioned components.
📝 IPlugin Interface
All plugins must implement the IPlugin interface. For convenience, you can inherit from PluginBase which provides helper methods.
Interface Definition
Here's the complete IPlugin interface with comments explaining each member:
public interface IPlugin
{
// ========================================================================
// REQUIRED PROPERTIES
// ========================================================================
// Unique identifier for this plugin (e.g., "BlazorBlueprint.Plugins.MyPlugin")
// Used internally to identify the plugin in settings and databases
// Must be unique across all plugins
string Id { get; }
// Display name shown in PluginManagement UI (e.g., "My Awesome Plugin")
// This is what users see when browsing available plugins
string Name { get; }
// Plugin version using semantic versioning (e.g., "1.0.0")
// Follows Major.Minor.Patch format
string Version { get; }
// Description of what the plugin does
// Shown in PluginManagement to help users understand the plugin's purpose
string Description { get; }
// Plugin author name (e.g., "Your Name" or "Your Company")
// Shown in PluginManagement for attribution
string Author { get; }
// Category helps organize plugins in the UI (e.g., "Communication", "Utility")
// Used for filtering and grouping in PluginManagement
string Category { get; }
// Whether this plugin requires a premium license
// Set to true if plugin requires payment or subscription
bool IsPremium { get; }
// Whether the plugin is currently enabled for the site
// Managed by PluginManager - don't set this manually
bool IsEnabled { get; set; }
// ========================================================================
// LIFECYCLE METHODS
// ========================================================================
// Called during application startup to register plugin services
// Register your services here using dependency injection
// Example: services.AddScoped<IMyService, MyService>();
Task RegisterServicesAsync(IServiceCollection services, IConfiguration configuration);
// Called during app configuration (after service registration)
// Use this to configure middleware, SignalR hubs, etc.
// Example: app.MapHub<MyHub>("/hubs/myhub");
Task ConfigureAppAsync(WebApplication app);
// Called after application is fully started
// Use this for initialization that requires all services to be ready
// Example: Initialize background workers, cache warmup, etc.
Task OnApplicationStartupAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default);
// ========================================================================
// INTEGRATION METHODS
// ========================================================================
// Returns navigation menu items for the main sidebar
// These links appear in the navigation menu for all users
// Return empty if plugin doesn't need navigation items
IEnumerable<PluginNavItem> GetNavigationItems(IServiceProvider? serviceProvider = null);
// Returns admin pages that appear in PluginManagement
// These pages are shown as quick action buttons/links
// Return empty if plugin doesn't have admin pages
IEnumerable<PluginAdminPage> GetAdminPages();
// Returns routes (pages) your plugin provides
// Each route maps a URL to a Blazor component
// Routes are automatically registered when plugin is enabled
IEnumerable<PluginRoute> GetRoutes();
// ========================================================================
// SITE SETUP
// ========================================================================
// Called when a new site is created
// Use this to set up default data, create default settings, etc.
// Returns true if setup succeeded, false otherwise
// Example: Create default chat groups, initialize AI models, etc.
Task<bool> SetupNewOrganisationAsync(Organisation organisation, string adminUserId = null, IServiceProvider? serviceProvider = null);
// ========================================================================
// LAYOUT COMPONENTS
// ========================================================================
// Returns layout components paired with the slot the host should render them in:
// Header (sticky-friendly, top of page-content scroll), Footer (below page body),
// or Floating (outside the layout — for position:fixed overlays like chat bubbles).
// Return empty if plugin doesn't need layout components
IEnumerable<LayoutComponentRegistration> GetLayoutComponents(IServiceProvider? serviceProvider = null);
}💡 Tip: Instead of implementing IPlugin directly, inherit from PluginBase. It provides default implementations for optional methods and helper methods that make plugin development much easier!
🚀 Creating Your First Plugin
Step 1: Create Plugin Project
# Create new Razor Class Library project
dotnet new razorclasslib -n BlazorBlueprint.Plugins.MyPlugin
cd BlazorBlueprint.Plugins.MyPlugin
# Add project references
dotnet add reference ..\..\Core\BlazorBlueprint.Application\BlazorBlueprint.Application.csproj
dotnet add reference ..\..\Core\BlazorBlueprint.Domain\BlazorBlueprint.Domain.csprojStep 2: Implement Plugin Class (Recommended: Use PluginBase)
💡 Tip: Inherit from PluginBase instead of implementing IPlugin directly. It provides helper methods and default implementations.
// Import necessary namespaces
using BlazorBlueprint.Application.Plugins; // PluginBase class
using BlazorBlueprint.Application.Interfaces; // IPlugin interface
using BlazorBlueprint.Domain.Constants; // ApplicationRoleConstants for authorization
using Microsoft.Extensions.DependencyInjection; // IServiceCollection for DI
using Microsoft.Extensions.Configuration; // IConfiguration for settings
namespace BlazorBlueprint.Plugins.MyPlugin;
/// <summary>
/// Main plugin class - inherits from PluginBase for convenience
/// PluginBase provides helper methods and default implementations
/// </summary>
public class MyPlugin : PluginBase
{
// ========================================================================
// REQUIRED PROPERTIES - These must be overridden
// ========================================================================
/// <summary>
/// Unique identifier for this plugin.
/// Convention: "BlazorBlueprint.Plugins.{PluginName}"
/// This ID must be unique across all plugins.
/// </summary>
public override string Id => "BlazorBlueprint.Plugins.MyPlugin";
/// <summary>
/// Display name shown in PluginManagement UI.
/// This is what users see in the plugin list.
/// </summary>
public override string Name => "My Plugin";
/// <summary>
/// Plugin version using semantic versioning (Major.Minor.Patch).
/// Update this when you release new versions.
/// </summary>
public override string Version => "1.0.0";
/// <summary>
/// Description of what the plugin does.
/// Shown in PluginManagement to help users understand the plugin.
/// </summary>
public override string Description => "A sample plugin that demonstrates basic functionality";
/// <summary>
/// Plugin author name.
/// Your name or organization name.
/// </summary>
public override string Author => "Your Name";
// ========================================================================
// OPTIONAL PROPERTIES - These have default implementations in PluginBase
// ========================================================================
/// <summary>
/// Category helps organize plugins in the UI.
/// Examples: "Communication", "E-commerce", "Social", "Utility"
/// </summary>
public override string Category => "Utility";
/// <summary>
/// Whether this is a premium/paid plugin.
/// Set to true if the plugin requires a license or subscription.
/// </summary>
public override bool IsPremium => false;
// ========================================================================
// REGISTER SERVICES (REQUIRED METHOD)
// ========================================================================
/// <summary>
/// Registers services your plugin needs in dependency injection.
/// This is called when the application starts up.
/// Services registered here can be injected into your pages and components.
/// </summary>
public override async Task RegisterServicesAsync(IServiceCollection services, IConfiguration configuration)
{
// Register your plugin's services here
// AddScoped means a new instance is created for each HTTP request
// Other options: AddSingleton (one instance for entire app), AddTransient (new instance every time)
services.AddScoped<IMyService, MyService>();
// You can also register:
// - Background services: services.AddHostedService<MyBackgroundService>();
// - Options/configuration: services.Configure<MyOptions>(options => { ... });
// - Repositories, other services, etc.
await Task.CompletedTask; // Required for async method
}
// ========================================================================
// REGISTER ROUTES (REQUIRED METHOD)
// ========================================================================
/// <summary>
/// Defines the routes (pages) your plugin provides.
/// Each route maps a URL to a Blazor component/page.
/// Routes are automatically registered when the plugin is enabled.
/// </summary>
public override IEnumerable<PluginRoute> GetRoutes()
{
// 1. Main plugin landing page (the "app dashboard")
// This is where users go when they click the plugin tile in /apps
// or the Apps link in the user-dropdown.
// Plugin's main entry — renders under MainLayout with the host's
// PluginSectionNav sidebar (replaces the platform NavMenu while inside the
// plugin). Settings + admin-only pages live under /admin/plugin/{pluginname}/*.
yield return new PluginRoute
{
Route = "/plugin/myplugin", // Canonical dashboard URL
ComponentType = typeof(Web.Pages.Admin.Index), // The Razor component to render
RequiresAuthentication = true, // Must be logged in
RequiredRole = ApplicationRoleConstants.ORGANISATIONADMIN // Must have ORGANISATIONADMIN role
};
// 2. Public route - accessible to everyone
// This page appears in the main navigation menu
// Route: /plugin/myplugin/mypage
// CreatePublicRoute is a helper method from PluginBase
// It automatically constructs the route: /plugin/{pluginname}/mypage
yield return CreatePublicRoute("mypage", typeof(Web.Pages.MyPage));
// 3. Admin route - settings page
// Only ORGANISATIONADMIN users can access this
// Route: /admin/plugin/myplugin/settings — renders under AdminLayout
// because it has a "deeper" path segment after the plugin name.
// CreateAdminRoute is a helper that constructs: /admin/plugin/{pluginname}/settings
yield return CreateAdminRoute("settings", typeof(Web.Pages.Admin.Settings), ApplicationRoleConstants.ORGANISATIONADMIN);
}
// ========================================================================
// NAVIGATION ITEMS (OPTIONAL METHOD)
// ========================================================================
/// <summary>
/// Adds items to the main navigation menu.
/// These links appear in the sidebar navigation for all users (or based on auth requirements).
/// This method is optional - omit it if your plugin doesn't need navigation items.
/// </summary>
public override IEnumerable<PluginNavItem> GetNavigationItems(IServiceProvider? serviceProvider = null)
{
// IMPORTANT: Choose ONE of the following options, not both!
// If you yield return multiple items with the same title, you'll get duplicates.
// ===== OPTION 1: Simple navigation item - direct link =====
// This creates a single menu item that links directly to your page
yield return CreateNavItem(
title: "My Plugin", // Text shown in menu
href: "/plugin/myplugin/mypage", // URL to navigate to
icon: "Settings", // Icon name (FluentUI icon)
order: 50, // Position in menu (lower = higher up)
requiresAuth: false // Whether user must be logged in
);
}
// ========================================================================
// ADMIN PAGES (OPTIONAL METHOD)
// ========================================================================
/// <summary>
/// Defines admin pages that appear in PluginManagement and your plugin's index page.
/// These pages are shown as quick action buttons/links.
/// This method is optional - omit it if your plugin doesn't have admin pages.
/// </summary>
public override IEnumerable<PluginAdminPage> GetAdminPages()
{
// These admin pages appear in the admin nav's Plugins group.
// Direct URL: /admin/plugin/myplugin/{pagename} — renders under AdminLayout.
// Only list genuinely admin / configuration pages here — user-facing dashboard
// pages live at /plugin/myplugin/* and are reached via the host PluginSectionNav.
// Settings page - order 100 (appears first)
yield return CreateAdminPage(
title: "Settings", // Display name
pageName: "settings", // URL segment (becomes /admin/plugin/myplugin/settings)
icon: "Settings", // Icon name for display
order: 100 // Display order (lower = appears first)
);
}
}💡 Why PluginBase? It provides helper methods like CreatePublicRoute(), CreateAdminRoute(), CreateNavItem(), and CreateAdminPage() that handle route conventions automatically. Much easier than manually constructing routes!
Step 3: Add to Solution
# Add plugin to solution
dotnet sln add BlazorBlueprint.Plugins.MyPluginStep 4: Reference in Web Project
Edit Web/BlazorBlueprint.Web/BlazorBlueprint.Web.csproj:
<ItemGroup>
<ProjectReference Include="..\..\Plugins\BlazorBlueprint.Plugins.MyPlugin\BlazorBlueprint.Plugins.MyPlugin.csproj" />
</ItemGroup>✅ That's it! Your plugin will be automatically discovered and loaded when you run the application.
📦 Installation & Configuration
Method 1: Project Reference (Development)
For development, add the plugin as a project reference in Web/BlazorBlueprint.Web/BlazorBlueprint.Web.csproj:
<ItemGroup>
<ProjectReference Include="..\..\Plugins\MyPlugin\MyPlugin.csproj" />
</ItemGroup>Method 2: Folder-Based (Production)
For production or distribution, place plugin assemblies in the application's Plugins/ folder (next to the running Web app).
When running from source, the loader also attempts to find Web/Plugins/.
Each plugin must live in its own sub-folder (the loader scans for Plugins/<PluginName>/*.dll, top-level DLLs only):
Web/
└── Plugins/
└── MyPlugin/
├── MyPlugin.dll
├── MyPlugin.deps.json
└── [dependencies]Plugins are discovered at startup (from already-loaded assemblies and from the folder scan). Use "Rescan Plugins Folder" in Plugin Management to make newly added plugin assemblies show up as available for installation.
Note: rescan registers plugin types (and their services), but plugin routes/hubs/middleware are wired up only during app startup via
ConfigurePluginsAsync. To activate public/admin routes and hub endpoints for a newly discovered plugin, restart the app.
Two-flag opt-in via appsettings.json
The host reads two independent flags per plugin from appsettings.json. Both default to false
so that simply dropping a plugin DLL into your app does NOT silently auto-modify your organisations.
"Plugins": {
"BlazorBlueprint.Plugins.Cms": {
"AutoInstall": true, // run SetupNewOrganisationAsync for every new org created via
// initial-setup or /Org/Select. false = the plugin is loaded
// and visible in /Admin/PluginManagement, but org admins must
// click Install manually per org.
"Disabled": false // emergency kill switch. true = RegisterServicesAsync is
// skipped, ConfigureAppAsync never runs, services are not in
// DI. Existing orgs' OrganisationPlugins blob is preserved
// but functionally inert. Use this to disable a misbehaving
// plugin in prod via config + restart, no redeploy required.
}
}| Configuration | Auto-install on new orgs | Visible in PluginManagement | Manual install button |
|---|---|---|---|
| DLL present, no appsettings entry | No | Yes | Yes |
"AutoInstall": true |
Yes | Yes | Yes (for orgs that pre-existed) |
"Disabled": true |
No | No | No |
Per-organisation install (PluginManagement admin)
- Navigate to Plugin Management
- Plugins listed under "Available" have not yet been installed for the current organisation. Plugins under "Installed" are already active.
- Click Install on an Available plugin to run its
SetupNewOrganisationAsyncfor the current org — the plugin's per-org settings, role definitions, and any seeded entities (e.g. CMS Content Writer model) are created. - Plugins flagged
"AutoInstall": trueappear under "Installed" automatically for orgs created after the flag was set; existing pre-flag orgs still need a manual Install click here. - The Disable button on an installed plugin removes the per-org entry from
OrganisationPluginsbut keeps the plugin globally loaded. Use the appsettings"Disabled": trueflag for a global kill switch.
✅ Best Practices
1. Plugin Isolation
- Don't modify core code - plugins should be self-contained
- Use interfaces for communication with core services
- Store all settings in
Organisation.InstalledPlugins(keyed by plugin Id)
2. Error Handling
- Wrap all plugin operations in try-catch blocks
- Don't let plugin errors crash the application
- Log errors with appropriate log levels
3. Route Conventions
- User-side pages:
/plugin/{pluginname}for the main entry and/plugin/{pluginname}/{route}for every daily-use page. Render underMainLayoutwith the host'sPluginSectionNavsidebar in place of the platformNavMenu. Anonymous-accessible plugin surfaces (e.g. LiveChat's/plugin/livechat/publicchat/{id}) live in the same namespace. - Admin / settings pages:
/admin/plugin/{pluginname}/{route}— settings, setup wizards (when they fit the admin model), and configuration pages. Render underAdminLayoutand are reached from the admin nav. - Cross-section rule: pages under
/plugin/{name}/*must not link to/admin/plugin/{name}/*. Reference admin pages by name in copy ("Admin → Plugins → CRM → Plugin settings") instead of an<a href>so the section split stays clean. - Use kebab-case for route names.
4. Service Lifetime
- Use
Scopedfor request-specific services - Use
Singletonfor shared state or cache - Create scopes in background services using
IServiceProvider
5. Data Access Conventions (Org vs Platform)
- Organisation-scoped data: use the normal tenant-scoped repository pattern (e.g.
IRepository<T>) so reads/writes land in the current tenant database. - Switch tenant context in background jobs: when a background service needs to iterate across organisations, it should:
1)fetch organisation metadata from the platform database (e.g.IPlatformRepository<Organisation>),2)setITenantContextSetterto the target organisation, then3)run tenant-scoped queries usingIRepository<T>. - Plugin settings storage: per-site settings are stored in
Organisation.InstalledPlugins, keyed by plugin Id. Presence of a key means the plugin is both installed and enabled for the org.
📚 Real-World Examples
LiveChat Plugin
The LiveChat Plugin is a fully functional production plugin located at Plugins/BlazorBlueprint.Plugins.LiveChat/.
It demonstrates all common plugin features:
- ✅ Navigation menu items (with children)
- ✅ Admin pages with different authorization levels
- ✅ Public and authenticated routes
- ✅ SignalR hub with real-time communication
- ✅ Plugin settings (per-site configuration)
- ✅ Background services
- ✅ Service registration and dependency injection
- ✅ Site setup and initialization
- ✅ Layout components (floating chat button)
💡 Perfect Reference: Study the LiveChat plugin's code to see how a production plugin is structured. It's well-commented and follows all best practices!
Complete Documentation
For a detailed step-by-step tutorial with full code examples, see the Plugin Development Guide.
The guide includes:
- ✅ Complete "Greetings Plugin" example from scratch
- ✅ Service interface and implementation examples
- ✅ Public and admin page examples with full code
- ✅ Navigation menu integration
- ✅ Settings management
- ✅ Build and test instructions
📦 Ready to Download?
Build custom plugins and extend the Blazor Blueprint starter template. Download the free Developer version or get Enterprise with premium plugins and commercial licensing.
🔓 Open Source on GitHub • Free for personal/non-commercial use • Enterprise license (£399) required for commercial use • Full source code included