Skip to content

Plugin Lifecycle ​

This document covers how plugins are loaded, enabled/disabled, and removed.

Loading Plugins ​

From Directory (Desktop) ​

csharp
// Load all plugins from a directory
await app.Services.UsePluginsAsync("plugins");

Discovery Process:

  1. Scans directory and subdirectories for DLLs
  2. Pre-loads all DLLs as dependencies
  3. Identifies assemblies with IPluginManifest implementations
  4. Creates PluginLoadContext for dependency isolation
  5. Loads manifest and discovers component types
  6. Registers with PluginState

Directory Structure:

plugins/
├── PluginA/
│   ├── PluginA.dll
│   └── PluginA.deps.json
└── PluginB/
    ├── PluginB.dll
    └── SomeDependency.dll

From Assembly (WebAssembly) ​

csharp
// Load from referenced assembly
await app.Services.UsePluginAsync(typeof(MyPlugin.Manifest).Assembly);

WebAssembly doesn't support dynamic assembly loading, so plugins must be referenced at compile time.

Plugin State ​

PluginState Manager ​

Singleton managing all loaded plugins:

csharp
public class PluginState
{
    // All loaded plugins
    public IReadOnlyList<PluginInfo> Plugins { get; }

    // Only enabled plugins (respects PluginsActive)
    public IReadOnlyList<PluginInfo> EnabledPlugins { get; }

    // Global enable/disable
    public bool PluginsActive { get; set; }

    // Enabled component types
    public IReadOnlyList<Type> EnabledMenuComponents { get; }
    public IReadOnlyList<Type> EnabledContextPanelComponents { get; }
}

PluginInfo ​

Runtime representation of a loaded plugin:

csharp
public class PluginInfo
{
    public IPluginManifest Manifest { get; }
    public Assembly Assembly { get; }
    public bool IsEnabled { get; set; }
    public DateTime LoadedAt { get; }
    public string? SourcePath { get; }

    // Discovered components
    public IReadOnlyList<Type> MenuComponents { get; }
    public IReadOnlyList<Type> ContextPanelComponents { get; }
}

Enable/Disable ​

Enabling a Plugin ​

csharp
var pluginState = services.GetRequiredService<PluginState>();
await pluginState.EnablePluginAsync("com.example.myplugin");

Flow:

  1. PluginEnabling event fires (cancellable)
  2. If not cancelled, IsEnabled set to true
  3. PluginEnabled event fires
  4. StateChanged event fires
  5. Components now render and receive messages

Disabling a Plugin ​

csharp
await pluginState.DisablePluginAsync("com.example.myplugin");

Flow:

  1. PluginDisabling event fires (cancellable)
  2. If not cancelled, IsEnabled set to false
  3. PluginDisabled event fires
  4. StateChanged event fires
  5. Components stop rendering, messages filtered

Global Toggle ​

csharp
// Disable all plugins
pluginState.PluginsActive = false;

// Re-enable all plugins
pluginState.PluginsActive = true;

Lifecycle Events ​

csharp
public class PluginState
{
    // Before enabling (can cancel)
    public event EventHandler<PluginLifecycleEventArgs>? PluginEnabling;

    // After enabling
    public event EventHandler<PluginLifecycleEventArgs>? PluginEnabled;

    // Before disabling (can cancel)
    public event EventHandler<PluginLifecycleEventArgs>? PluginDisabling;

    // After disabling
    public event EventHandler<PluginLifecycleEventArgs>? PluginDisabled;

    // Any state change
    public event EventHandler? StateChanged;
}

Cancellation Example:

csharp
pluginState.PluginDisabling += (sender, args) =>
{
    if (args.Plugin.Manifest.Id == "critical.plugin")
    {
        args.Cancel = true;  // Prevent disabling
    }
};

Removing Plugins ​

csharp
await pluginState.RemovePluginAsync("com.example.myplugin");

Flow:

  1. Disables plugin if enabled
  2. Removes from plugin list
  3. StateChanged event fires
  4. Optionally prompts for data deletion (if IPersistentPlugin)

Lifecycle Diagram ​

                    ┌─────────────┐
                    │   DLL File  │
                    └──────┬──────┘
                           │
                    PluginLoader.LoadPlugin()
                           │
                           ▼
                    ┌─────────────┐
                    │  PluginInfo │
                    │ (Disabled)  │
                    └──────┬──────┘
                           │
              PluginState.RegisterPluginAsync()
                           │
                           ▼
                    ┌─────────────┐
                    │  Registered │◄────────────────────┐
                    │  (Enabled)  │                     │
                    └──────┬──────┘                     │
                           │                            │
         ┌─────────────────┼─────────────────┐          │
         │                 │                 │          │
  DisablePluginAsync() StateChanged()  EnablePluginAsync()
         │                 │                 │          │
         ▼                 ▼                 ▼          │
    ┌─────────────┐  ┌───────────┐    ┌───────────┐    │
    │  Disabled   │  │    UI     │    │  Enabled  │────┘
    │(no messages)│  │  Updates  │    │(messages) │
    └──────┬──────┘  └───────────┘    └───────────┘
           │
   RemovePluginAsync()
           │
           ▼
    ┌─────────────┐
    │   Removed   │
    │(cleanup opt)│
    └─────────────┘

Message Bus Filtering ​

DisabledPluginConsumerFilter prevents disabled plugins from receiving messages:

csharp
public class DisabledPluginConsumerFilter : IConsumerFilter
{
    public bool ShouldInvoke<TMessage>(IConsumer<TMessage> consumer, TMessage message)
    {
        // Returns false if consumer's assembly belongs to disabled plugin
        // Also returns false if PluginsActive is false
    }
}

This is automatically registered when using AddPluginFramework().