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().