Hooks
Module lifecycle behavior has three extension points:
- Override the virtual lifecycle methods on
Module<T>for behavior owned by one module. - Implement the attribute interfaces in
ModularPipelines.Eventsfor reusable, opt-in behavior attached to selected modules. - Implement
IModuleEventHandlerfor behavior that observes every module in a pipeline.
ModuleConfiguration controls execution policy only; it does not contain lifecycle hooks.
Module virtual hooks
Override the virtual methods directly when the behavior belongs to the module:
public class MyModule : Module<string>
{
protected override Task OnBeforeExecuteAsync(
IModuleContext context,
CancellationToken cancellationToken)
{
context.Logger.LogInformation("Setting up MyModule");
return Task.CompletedTask;
}
protected override Task<ModuleResult<string>?> OnAfterExecuteAsync(
IModuleContext context,
ModuleResult<string> result,
CancellationToken cancellationToken)
{
context.Logger.LogInformation("MyModule completed");
return Task.FromResult<ModuleResult<string>?>(null);
}
protected override Task OnSkippedAsync(
IModuleContext context,
SkipDecision skipDecision,
CancellationToken cancellationToken)
{
context.Logger.LogInformation("Skipped: {Reason}", skipDecision.Reason);
return Task.CompletedTask;
}
protected override Task OnFailedAsync(
IModuleContext context,
Exception exception,
CancellationToken cancellationToken)
{
context.Logger.LogError(exception, "MyModule failed");
return Task.CompletedTask;
}
protected internal override Task<string> ExecuteAsync(
IModuleContext context,
CancellationToken cancellationToken)
=> Task.FromResult<string?>("Hello, World!");
}
OnBeforeExecuteAsync runs once before the first execution attempt.
OnAfterExecuteAsync runs once after the final attempt and can return a replacement result;
return null to retain the original result. OnFailedAsync runs before
OnAfterExecuteAsync when execution fails.
Attribute event handlers
Implement an event-handler interface on an attribute, then apply it to selected modules:
[AttributeUsage(AttributeTargets.Class)]
public sealed class AuditModuleAttribute : Attribute,
IModuleStartHandler,
IModuleEndHandler
{
public Task OnModuleStartAsync(IModuleHookContext context)
{
context.Logger.LogInformation("{Module} started", context.ModuleName);
return Task.CompletedTask;
}
public Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result)
{
context.Logger.LogInformation("{Module} ended", context.ModuleName);
return Task.CompletedTask;
}
}
[AuditModule]
public class BuildModule : Module<string>
{
// ...
}
Available interfaces are IModuleReadyHandler, IModuleStartHandler,
IModuleEndHandler, IModuleFailureHandler, and IModuleSkippedHandler.
All handlers inherit IEventHandler. Set Priority to control order (lower values run
first), or ContinueOnError to log a handler failure and continue.
Registration attributes implement IModuleRegistrationHandler. Also implement
IPlanningSafeModuleRegistrationHandler only for deterministic, idempotent handlers
without external side effects; those handlers may run while exporting a resolved
dependency graph.
Global module event handlers
Implement IModuleEventHandler to observe every module, then register it once:
public sealed class ModuleMetricsHandler : IModuleEventHandler
{
public Task OnModuleStartAsync(IModuleHookContext context)
{
context.Logger.LogInformation("{Module} started", context.ModuleName);
return Task.CompletedTask;
}
public Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result)
{
context.Logger.LogInformation(
"{Module} finished after {Elapsed}",
context.ModuleName,
context.ElapsedTime);
return Task.CompletedTask;
}
}
builder.AddModuleEventHandler<ModuleMetricsHandler>();
Global and attribute handlers use the same callback signatures and shared error/priority properties. Global handlers run sequentially in priority order for each event.
Lifecycle ordering
The order for a successful module is:
- Global
OnModuleReadyAsync - Attribute
IModuleReadyHandler - Global
OnModuleStartAsync - Attribute
IModuleStartHandler - Module
OnBeforeExecuteAsync - Module
ExecuteAsyncthrough its configured resilience shield - Module
OnAfterExecuteAsync - Global
OnModuleEndAsync - Attribute
IModuleEndHandler
For a failed execution, the completion portion is:
- Module
OnFailedAsync - Module
OnAfterExecuteAsync - Attribute
IModuleFailureHandler - Global
OnModuleFailureAsync
For a skipped module, the completion portion is:
- Module
OnSkippedAsync - Attribute
IModuleSkippedHandler - Global
OnModuleSkippedAsync
If OnBeforeExecuteAsync throws, ExecuteAsync and OnAfterExecuteAsync do not run;
OnFailedAsync and the failure event handlers are still notified. Exceptions from
OnAfterExecuteAsync, OnFailedAsync, and OnSkippedAsync are logged without replacing
the module outcome.
Pipeline event handlers
IPipelineEventHandler observes the pipeline as a whole rather than individual modules:
public sealed class PipelineLoggingHandler : IPipelineEventHandler
{
public Task OnPipelineStartAsync(IPipelineContext context)
{
context.Logger.LogInformation("Pipeline started");
return Task.CompletedTask;
}
public Task OnPipelineEndAsync(
IPipelineContext context,
PipelineSummary summary)
{
context.Logger.LogInformation("Pipeline ended");
return Task.CompletedTask;
}
}
builder.AddPipelineEventHandler<PipelineLoggingHandler>();
Pipeline handlers also inherit IEventHandler and run in priority order.