Skip to content

[Task] Add Plugin Lifecycle Hooks and Hosted Services #10

Description

@rian-be

Summary

Extend the AuthKit.Plugins.Abstractions plugin contract with explicit application lifecycle hooks and hosted service integration.

The goal is to allow plugins to safely initialize runtime resources, perform post start operations, and release resources during graceful shutdown while using the standard .NET hosting lifecycle.

The implementation must remain additive and preserve compatibility with existing plugins.

Goal

Provide first class plugin lifecycle support for:

  • startup initialization,
  • post start notification,
  • graceful shutdown,
  • and plugin provided IHostedService instances.

Lifecycle operations must be deterministic, cancellation aware, and integrated with the normal ASP.NET Core/.NET host lifecycle.

Background

Plugins may need to allocate or initialize resources that cannot be handled during static service registration.

Examples include

External connections
Background workers
Caches
Runtime state
External registrations
Resource cleanup

Without explicit lifecycle hooks, plugins are forced to rely on application specific startup behavior or hosted service workarounds.

The plugin contract should expose explicit lifecycle points while avoiding the creation of custom lifecycle framework.

Scope

B6. Plugin Lifecycle Hooks

Add asynchronous lifecycle hooks:

Task OnStartingAsync(
    CancellationToken cancellationToken);

Task OnStartedAsync(
    CancellationToken cancellationToken);

Task OnStoppingAsync(
    CancellationToken cancellationToken);

The exact naming may follow the existing AuthKit naming conventions, but the semantics must correspond to:

  • Starting - before the application is considered started;
  • Started - after successful host/application startup;
  • Stopping - during graceful application shutdown.

If the existing specification requires the names OnStarting, OnStarted, and OnStopping, those names should be retained while using asynchronous return types.

B7. GetHostedServices

Add:

IReadOnlyList<IHostedService> GetHostedServices();

Plugins may return one or more hosted services that should participate in the standard .NET host lifecycle.

The AuthKit host is responsible for registering these services with DI.

Lifecycle Model

The expected lifecycle is:

Host Construction
      │
      ▼
Plugin Configuration
      │
      ▼
Host Build
      │
      ▼
OnStarting
      │
      ▼
Host Start
      │
      ├── IHostedService.StartAsync
      │
      ▼
Application Started
      │
      ▼
OnStarted
      │
      ▼
Running
      │
      ▼
Shutdown Requested
      │
      ▼
OnStopping
      │
      ├── IHostedService.StopAsync
      │
      ▼
Host Stopped

The exact relationship between plugin lifecycle hooks and the standard IHostedService lifecycle must be explicitly defined and tested by the host.

B6. OnStarting

OnStarting is intended for plugin initialization that must occur before the application is considered fully started.

Typical uses include:

Validate external resources
Initialize plugin runtime state
Warm required caches
Prepare connections

The method must support cancellation.

If cancellation is requested, the plugin must be allowed to stop its startup operation.
A plugin must not be expected to continue performing potentially blocking startup work after cancellation has been requested.

Startup Failure

If OnStarting fails for required plugin operation, the host must treat startup as failed.

The host must not silently continue as if the plugin initialized successfully.
The resulting exception must be observable through the normal host startup failure mechanism.

B6. OnStarted

OnStarted is called after the application has successfully started.

This hook is intended for operations that should only execute once the host is running.

Examples:

Start non critical synchronization
Notify external systems
Perform post-start registration
Initialize optional background state

OnStarted must not be invoked if application startup failed.

The host must ensure that the hook is invoked at most once per host lifecycle.

B6. OnStopping

OnStopping is called when the host begins graceful shutdown.

It is intended for plugin cleanup and shutdown preparation.

Examples:

Stop accepting plugin specific work
Flush buffered data
Release external registrations
Prepare resources for disposal

The cancellation token supplied to the hook must represent the host shutdown cancellation semantics.

The host must not indefinitely block shutdown because plugin ignores cancellation.
The exact shutdown timeout behavior should follow the standard .NET host configuration.

Cancellation

All asynchronous lifecycle hooks must support CancellationToken.

The host must pass the appropriate token from the application's lifecycle.
The host must not create unrelated cancellation tokens that disconnect plugin operations from the actual host lifecycle.

Tests must verify that cancellation is propagated.

Invocation Ordering

Lifecycle hook ordering must be deterministic.

For multiple plugins:

Plugin A OnStarting
Plugin B OnStarting
Plugin A OnStarted
Plugin B OnStarted

or another explicitly documented ordering is acceptable.

However, the host must not rely accidentally on:

  • filesystem order,
  • assembly load order,
  • dictionary enumeration order,
  • or undefined plugin discovery order.

The chosen ordering must be documented and tested.
The same principle applies to OnStopping.

Failure Isolation

Lifecycle failures must be handled explicitly.

If one plugin fails during startup:

Plugin A → success
Plugin B → failure
Plugin C → ...

the host must follow defined startup failure policy.

It must not silently mark Plugin B as healthy.

If cleanup is required for already started plugins, the host should follow the normal .NET graceful shutdown/failure semantics.

The exact failure policy must be documented by the implementation.

B7. Hosted Services

Plugins may return hosted services through:

IReadOnlyList<IHostedService> GetHostedServices();

The returned services must integrate with the standard .NET hosting infrastructure.

The host should register them through the existing DI container rather than manually invoking:

StartAsync
StopAsync

outside the normal host lifecycle.

Hosted Service Registration

For every plugin:

GetHostedServices()
       ↓
IHostedService instances
       ↓
DI registration
       ↓
.NET Host lifecycle

The host must ensure that returned services are registered before host startup.

Hosted services must participate in normal:

StartAsync
StopAsync

processing.

Hosted Service Lifetime

The implementation must define the expected lifetime of plugin provided hosted services.

Services should not be independently disposed by the plugin contract if they are registered with the application's DI container.

DI should remain responsible for their lifecycle.

The plugin should therefore not need to manually track and dispose services returned from GetHostedServices() unless the contract explicitly requires it.

Duplicate Hosted Services

The host must avoid accidental duplicate registration.

Calling plugin discovery/configuration more than once must not result in the same hosted service being registered multiple times.

If multiple distinct instances are intentionally returned by the plugin, the behavior must be documented.

Null and Invalid Results

GetHostedServices() must not return null.

If the plugin has no hosted services, it should return an empty collection:

Array.Empty<IHostedService>()

The host may defensively reject null result with an explicit contract/configuration error if the public API cannot guarantee non-nullability.

The host must not silently ignore invalid hosted service registrations.

Dependency Injection

Hosted services must use the existing AuthKit DI container.

Do not introduce:

  • a second service provider,
  • a custom service scope system,
  • or a custom hosted service scheduler.

Dependencies required by plugin hosted services should be resolved through the application's normal DI infrastructure.

Compatibility

The implementation must be additive.

Existing plugins must continue to compile and load without modification.
Plugins that do not implement lifecycle hooks must remain valid.
Plugins that do not provide hosted services must remain valid.
Default interface implementations should be used where appropriate.

For example, optional lifecycle hooks may safely default to:

Task.CompletedTask

and the hosted service collection may default to:

Array.Empty<IHostedService>()

The exact compatibility mechanism must match the target framework and existing IAuthKitPlugin design.

Thread Safety and Reentrancy

The host must prevent duplicate lifecycle invocation.

A plugin must not receive:

OnStarting
OnStarting

for the same application lifecycle unless explicitly supported.

Similarly:

OnStarted
OnStarted

must not occur accidentally.

The host must also prevent OnStarted from being invoked after OnStopping has already begun.

Runtime State

Lifecycle hooks may maintain plugin runtime state.
However, the plugin contract must not require plugins to expose mutable lifecycle state publicly.
The host should treat lifecycle methods as lifecycle notifications rather than state-management APIs.

Logging and Diagnostics

Lifecycle failures should provide sufficient diagnostic information to identify:

  • plugin name,
  • lifecycle stage,
  • underlying exception.

For example:

Plugin 'ExamplePlugin' failed during OnStarting.

The host should use its existing logging infrastructure.
Do not introduce separate plugin logging framework.

Testing Strategy

Tests should use sample plugin that records lifecycle events.

Example:

OnStarting
OnStarted
OnStopping

The test host should verify the exact order.

A second sample plugin should provide one or more hosted services and record:

HostedService.StartAsync
HostedService.StopAsync

The integration test should verify participation in the standard host lifecycle.

Validation

B6. Lifecycle Hooks

  • OnStarting exists.
  • OnStarted exists.
  • OnStopping exists.
  • Lifecycle hooks are asynchronous.
  • Lifecycle hooks accept CancellationToken.
  • OnStarting executes at the defined startup stage.
  • OnStarted executes only after successful application startup.
  • OnStopping executes during graceful shutdown.
  • Hooks execute in deterministic order.
  • Hooks are not invoked more than once per lifecycle.
  • Cancellation is propagated.
  • Lifecycle failures are surfaced explicitly.

B7. Hosted Services

  • GetHostedServices() exists.
  • The method returns IReadOnlyList<IHostedService>.
  • Empty hosted service collections are supported.
  • Plugin hosted services are registered with the application's DI container.
  • Hosted services start through the standard .NET host lifecycle.
  • Hosted services stop through the standard .NET host lifecycle.
  • Duplicate registration is prevented.
  • Hosted service failures are surfaced through normal host behavior.

Compatibility

  • Existing plugins compile without modification.
  • Existing plugins load without modification.
  • Plugins without lifecycle hooks remain valid.
  • Plugins without hosted services remain valid.
  • DevTokens continues to compile and load successfully.
  • PluginContractValidator passes.

Documentation

  • All new public APIs contain complete XML documentation.
  • Lifecycle ordering is documented.
  • Cancellation semantics are documented.
  • Hosted service registration semantics are documented.
  • Failure behavior is documented.

Build and Tests

  • dotnet build AuthKit.Plugins.Abstractions succeeds.
  • Build completes with zero warnings.
  • Relevant unit tests pass.
  • Lifecycle integration tests pass.
  • Hosted service integration tests pass.
  • Cancellation tests pass.
  • Failure path tests pass.
  • Duplicate invocation/registration tests pass.

Acceptance Criteria

  • Plugins can participate in host startup through OnStarting.
  • Plugins receive post start notification through OnStarted.
  • Plugins receive graceful shutdown notification through OnStopping.
  • All lifecycle hooks are asynchronous and cancellation aware.
  • Lifecycle invocation order is deterministic.
  • Lifecycle hooks cannot be accidentally invoked multiple times.
  • Lifecycle failures are surfaced explicitly.
  • Plugins can provide IHostedService implementations.
  • Plugin hosted services participate in the standard .NET host lifecycle.
  • Hosted services use the existing DI infrastructure.
  • No custom lifecycle or hosted service framework is introduced.
  • Existing plugins remain fully backward compatible.
  • PluginContractValidator passes.
  • DevTokens continues to compile and load successfully.

Non Goals

This task does not include:

  • endpoint mapping,
  • middleware configuration,
  • middleware pipeline positioning,
  • plugin configuration binding,
  • OpenAPI configuration,
  • Marten configuration,
  • authentication configuration,
  • authorization configuration,
  • redesigning IAuthKitPlugin,
  • or introducing custom application lifecycle framework.

These concerns belong to the other Section B issues.

Parent

Part of: Section B — Host Lifecycle and Hooks

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1Core operationadditiveAdditive, non-breaking changearea/abstractionsAuthKit.Plugins.Abstractions contractcontractChanges the plugin contractsub-taskChild task of an epic

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions