Plugin Development - Blazor Blueprint Custom Plugin Creation Guide
Login Register

🎯 Overview

BlazorBlueprint uses a modular plugin system that allows you to extend functionality without modifying core code. Plugins are self-contained, independently versioned components.

Plugin Management →

📝 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.csproj

Step 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.MyPlugin

Step 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)

  1. Navigate to Plugin Management
  2. Plugins listed under "Available" have not yet been installed for the current organisation. Plugins under "Installed" are already active.
  3. Click Install on an Available plugin to run its SetupNewOrganisationAsync for the current org — the plugin's per-org settings, role definitions, and any seeded entities (e.g. CMS Content Writer model) are created.
  4. Plugins flagged "AutoInstall": true appear under "Installed" automatically for orgs created after the flag was set; existing pre-flag orgs still need a manual Install click here.
  5. The Disable button on an installed plugin removes the per-org entry from OrganisationPlugins but keeps the plugin globally loaded. Use the appsettings "Disabled": true flag 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 under MainLayout with the host's PluginSectionNav sidebar in place of the platform NavMenu. 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 under AdminLayout and 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 Scoped for request-specific services
  • Use Singleton for 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) set ITenantContextSetter to the target organisation, then 3) run tenant-scoped queries using IRepository<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

Welcome! How can we help you today?
An unhandled error has occurred. Reload