diff --git a/docs/architecture/interface-audit.md b/docs/architecture/interface-audit.md index 2b0134292ef..2e98a64e3eb 100644 --- a/docs/architecture/interface-audit.md +++ b/docs/architecture/interface-audit.md @@ -15,7 +15,7 @@ IPipelineContext module-execution operations, while `IModuleHookContext` adds module lifecycle information. -Pipeline global hooks, requirements, and run conditions use `IPipelineContext`. +Pipeline event handlers, requirements, and run conditions use `IPipelineContext`. Module lifecycle hooks use `IModuleHookContext`. ## Capability interfaces @@ -36,8 +36,8 @@ marker were removed. Each capability now has one public name. ## Extension points -- `IPipelineGlobalHooks`: pipeline start and end callbacks -- `IModuleEventReceiver`: module lifecycle callbacks +- `IPipelineEventHandler`: pipeline start and end callbacks +- `IModuleEventHandler`: module lifecycle callbacks - `IPipelineRequirement`: startup requirement checks - `IRunCondition`: reusable execution conditions - `IPipelineValidator`: custom pipeline validation diff --git a/docs/architecture/interface-hierarchy.md b/docs/architecture/interface-hierarchy.md index 7705a00f055..5e87c98bbfd 100644 --- a/docs/architecture/interface-hierarchy.md +++ b/docs/architecture/interface-hierarchy.md @@ -63,12 +63,12 @@ public class BuildModule : Module } ``` -## Pipeline hooks +## Pipeline event handlers -Global hooks receive `IPipelineContext`: +Pipeline handlers receive `IPipelineContext`: ```csharp -public class PipelineHooks : IPipelineGlobalHooks +public class PipelineEvents : IPipelineEventHandler { public Task OnPipelineStartAsync(IPipelineContext context) { @@ -86,10 +86,10 @@ public class PipelineHooks : IPipelineGlobalHooks } ``` -Global module event receivers receive `IModuleHookContext`: +Global module event handlers use the same lifecycle signatures as attribute handlers: ```csharp -public class ModuleEvents : IModuleEventReceiver +public class ModuleEvents : IModuleEventHandler { public Task OnModuleStartAsync(IModuleHookContext context) { @@ -97,7 +97,7 @@ public class ModuleEvents : IModuleEventReceiver return Task.CompletedTask; } - public Task OnModuleEndAsync(IModuleHookContext context) + public Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result) { context.Logger.LogInformation("Module finished"); return Task.CompletedTask; @@ -108,7 +108,7 @@ public class ModuleEvents : IModuleEventReceiver ## Requirements and run conditions Pipeline requirements and run conditions receive `IPipelineContext`, giving them the -same shared capability surface as global hooks: +same shared capability surface as global handlers: ```csharp public class LinuxRequirement : IPipelineRequirement @@ -126,7 +126,7 @@ public class LinuxRequirement : IPipelineRequirement ## Guidance 1. Use `IModuleContext` in modules. -2. Use `IPipelineContext` in global hooks, requirements, and run conditions. -3. Use `IModuleHookContext` in module event receivers and attribute handlers. +2. Use `IPipelineContext` in pipeline event handlers, requirements, and run conditions. +3. Use `IModuleHookContext` in global and attribute module event handlers. 4. Use domain properties to discover capabilities. 5. Do not depend on internal engine interfaces. diff --git a/docs/docs/architecture/module-execution-lifecycle.md b/docs/docs/architecture/module-execution-lifecycle.md index 097b59624e8..f28e96c33b0 100644 --- a/docs/docs/architecture/module-execution-lifecycle.md +++ b/docs/docs/architecture/module-execution-lifecycle.md @@ -5,22 +5,22 @@ title: Module Execution Lifecycle # Module execution lifecycle A module combines execution policy, module-owned virtual hooks, opt-in attribute handlers, -and global event receivers. +and global event handlers. ## Execution phases For a module that runs successfully, the phases are: 1. Dependencies become ready. -2. Global `IModuleEventReceiver.OnModuleReadyAsync` receivers run concurrently. +2. Global `IModuleEventHandler.OnModuleReadyAsync` handlers run sequentially by priority. 3. Attribute `IModuleReadyHandler` handlers run sequentially by priority. -4. Global `IModuleEventReceiver.OnModuleStartAsync` receivers run concurrently. +4. Global `IModuleEventHandler.OnModuleStartAsync` handlers run sequentially by priority. 5. Attribute `IModuleStartHandler` handlers run sequentially by priority. 6. The module skip condition is evaluated. 7. `Module.OnBeforeExecuteAsync` runs once. 8. `Module.ExecuteAsync` runs through timeout handling and the configured resilience shield, which may compose retries with other resilience strategies. 9. `Module.OnAfterExecuteAsync` runs once. -10. Global `IModuleEventReceiver.OnModuleEndAsync` receivers run concurrently. +10. Global `IModuleEventHandler.OnModuleEndAsync` handlers run sequentially by priority. 11. Attribute `IModuleEndHandler` handlers run sequentially by priority. 12. The module result is published and dependants become eligible. @@ -34,7 +34,7 @@ decision: 1. `Module.OnSkippedAsync` 2. Attribute `IModuleSkippedHandler` -3. Global `IModuleEventReceiver.OnModuleSkippedAsync` +3. Global `IModuleEventHandler.OnModuleSkippedAsync` `OnBeforeExecuteAsync`, `ExecuteAsync`, and `OnAfterExecuteAsync` do not run. @@ -45,7 +45,7 @@ When module execution throws: 1. `Module.OnFailedAsync` 2. `Module.OnAfterExecuteAsync`, with a failed `ModuleResult` 3. Attribute `IModuleFailureHandler` -4. Global `IModuleEventReceiver.OnModuleFailureAsync` +4. Global `IModuleEventHandler.OnModuleFailureAsync` Retry attempts complete before this failure sequence. If the configured failure condition ignores the failure, the resulting module status reflects that policy. @@ -53,17 +53,18 @@ ignores the failure, the resulting module status reflects that policy. ## Hook failures - An exception from `OnBeforeExecuteAsync` prevents module execution. `OnFailedAsync` and the - failure event receivers are notified, but `OnAfterExecuteAsync` does not run. + failure event handlers are notified, but `OnAfterExecuteAsync` does not run. - Exceptions from `OnFailedAsync`, `OnSkippedAsync`, and `OnAfterExecuteAsync` are logged and do not replace the module outcome. -- Attribute handlers propagate by default. Set their `ContinueOnError` property to continue after - a handler failure. -- Exceptions from global event receivers propagate from the lifecycle event. +- Attribute and global handlers all run in ascending `Priority` order within their registration + family, even after a handler fails. `ContinueOnError` controls failure propagation: `false` + rethrows one recorded failure or aggregates multiple failures after dispatch; `true` suppresses + that handler's failure. ## Choosing an extension point Use module virtual hooks when behavior is part of one module. Use attribute handlers when -behavior should be explicitly attached to selected module types. Use `IModuleEventReceiver` +behavior should be explicitly attached to selected module types. Use `IModuleEventHandler` when one service must observe every module in the pipeline. See [Hooks](../how-to/hooks.md) for implementation examples. diff --git a/docs/docs/how-to/hooks.md b/docs/docs/how-to/hooks.md index 3b89f7c6d4c..55f2c8b1054 100644 --- a/docs/docs/how-to/hooks.md +++ b/docs/docs/how-to/hooks.md @@ -7,9 +7,9 @@ title: Hooks Module lifecycle behavior has three extension points: 1. Override the virtual lifecycle methods on `Module` for behavior owned by one module. -2. Implement the attribute interfaces in `ModularPipelines.Attributes.Events` for reusable, +2. Implement the attribute interfaces in `ModularPipelines.Events` for reusable, opt-in behavior attached to selected modules. -3. Implement `IModuleEventReceiver` for behavior that observes every module in a pipeline. +3. Implement `IModuleEventHandler` for behavior that observes every module in a pipeline. `ModuleConfiguration` controls execution policy only; it does not contain lifecycle hooks. @@ -99,14 +99,20 @@ public class BuildModule : Module Available interfaces are `IModuleReadyHandler`, `IModuleStartHandler`, `IModuleEndHandler`, `IModuleFailureHandler`, and `IModuleSkippedHandler`. -Handlers can implement `IEventHandlerPriority`; lower values run first. +All handlers inherit `IEventHandler`. Set `Priority` to control order (lower values run +first), or `ContinueOnError` to log a handler failure and continue. -## Global module event receivers +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. -Implement `IModuleEventReceiver` to observe every module, then register it once: +## Global module event handlers + +Implement `IModuleEventHandler` to observe every module, then register it once: ```csharp -public sealed class ModuleMetricsReceiver : IModuleEventReceiver +public sealed class ModuleMetricsHandler : IModuleEventHandler { public Task OnModuleStartAsync(IModuleHookContext context) { @@ -114,7 +120,7 @@ public sealed class ModuleMetricsReceiver : IModuleEventReceiver return Task.CompletedTask; } - public Task OnModuleEndAsync(IModuleHookContext context) + public Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result) { context.Logger.LogInformation( "{Module} finished after {Elapsed}", @@ -124,11 +130,11 @@ public sealed class ModuleMetricsReceiver : IModuleEventReceiver } } -builder.AddModuleEventReceiver(); +builder.AddModuleEventHandler(); ``` -All registered global receivers are invoked concurrently for each event. Attribute handlers -run sequentially in priority order. +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 @@ -158,16 +164,16 @@ For a skipped module, the completion portion is: 3. Global `OnModuleSkippedAsync` If `OnBeforeExecuteAsync` throws, `ExecuteAsync` and `OnAfterExecuteAsync` do not run; -`OnFailedAsync` and the failure event receivers are still notified. Exceptions from +`OnFailedAsync` and the failure event handlers are still notified. Exceptions from `OnAfterExecuteAsync`, `OnFailedAsync`, and `OnSkippedAsync` are logged without replacing the module outcome. -## Pipeline hooks +## Pipeline event handlers -`IPipelineGlobalHooks` observes the pipeline as a whole rather than individual modules: +`IPipelineEventHandler` observes the pipeline as a whole rather than individual modules: ```csharp -public sealed class PipelineLoggingHooks : IPipelineGlobalHooks +public sealed class PipelineLoggingHandler : IPipelineEventHandler { public Task OnPipelineStartAsync(IPipelineContext context) { @@ -184,5 +190,7 @@ public sealed class PipelineLoggingHooks : IPipelineGlobalHooks } } -builder.AddPipelineGlobalHooks(); +builder.AddPipelineEventHandler(); ``` + +Pipeline handlers also inherit `IEventHandler` and run in priority order. diff --git a/docs/docs/how-to/pipeline-host.md b/docs/docs/how-to/pipeline-host.md index 9bb9bcf3eae..76e3ff4e435 100644 --- a/docs/docs/how-to/pipeline-host.md +++ b/docs/docs/how-to/pipeline-host.md @@ -243,18 +243,18 @@ builder await builder.RunAsync(); ``` -## Hooks and Requirements +## Event Handlers and Requirements -Register global hooks and pipeline requirements: +Register event handlers and pipeline requirements: ```csharp var builder = Pipeline.CreateBuilder(args); -// Global hooks (run before/after all modules) -builder.AddPipelineGlobalHooks(); +// Pipeline event handlers (run before/after all modules) +builder.AddPipelineEventHandler(); -// Module event receivers (observe every module) -builder.AddModuleEventReceiver(); +// Module event handlers (observe every module) +builder.AddModuleEventHandler(); // Requirements (validated before pipeline starts) builder.AddRequirement(); diff --git a/src/ModularPipelines.GitHub/Extensions/GitHubExtensions.cs b/src/ModularPipelines.GitHub/Extensions/GitHubExtensions.cs index a182c3d2721..f3dfe122bee 100644 --- a/src/ModularPipelines.GitHub/Extensions/GitHubExtensions.cs +++ b/src/ModularPipelines.GitHub/Extensions/GitHubExtensions.cs @@ -4,6 +4,7 @@ using ModularPipelines.Attributes; using ModularPipelines.Context; using ModularPipelines.Engine; +using ModularPipelines.Events; using ModularPipelines.GitHub.PipelineWriters; using ModularPipelines.Interfaces; using ModularPipelines.Modules; @@ -38,7 +39,7 @@ public static IServiceCollection RegisterGitHubContext(this IServiceCollection s services.TryAddScoped(); services.TryAddScoped(); services.TryAddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.TryAddEnumerable( ServiceDescriptor.Singleton()); services.AddGitHubHttpClient(); diff --git a/src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs b/src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs index 759d16d308a..6e49af49bd3 100644 --- a/src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs +++ b/src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs @@ -2,13 +2,14 @@ using ModularPipelines.Context; using ModularPipelines.Engine; using ModularPipelines.Enums; +using ModularPipelines.Events; using ModularPipelines.Interfaces; using ModularPipelines.Logging; using ModularPipelines.Models; namespace ModularPipelines.GitHub; -internal class GitHubMarkdownSummaryGenerator : IPipelineGlobalHooks +internal class GitHubMarkdownSummaryGenerator : IPipelineEventHandler { private const long MaxFileSizeInBytes = 1 * 1024 * 1024; // 1MB diff --git a/src/ModularPipelines/AmbientModuleContext.cs b/src/ModularPipelines/AmbientModuleContext.cs index 19bbb2f032d..33f8cd133f3 100644 --- a/src/ModularPipelines/AmbientModuleContext.cs +++ b/src/ModularPipelines/AmbientModuleContext.cs @@ -40,7 +40,7 @@ public static class AmbientModuleContext /// During module execution (via the module runner) /// /// It returns null when code is executing outside of any module context, - /// such as during pipeline initialization or in global hooks. + /// such as during pipeline initialization or in pipeline event handlers. /// public static Type? CurrentModuleType => ModuleLogger.CurrentModuleType.Value; diff --git a/src/ModularPipelines/Attributes/Events/IEventHandlerPriority.cs b/src/ModularPipelines/Attributes/Events/IEventHandlerPriority.cs deleted file mode 100644 index 8585f88d6ae..00000000000 --- a/src/ModularPipelines/Attributes/Events/IEventHandlerPriority.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an event receiver attribute to specify its execution priority. -/// Lower priority values run first (priority 0 runs before priority 100). -/// -/// -/// -/// When multiple event receivers of the same type are applied to a module, -/// they are invoked in order of their priority. Receivers with lower priority -/// values execute before those with higher values. -/// -/// -/// Receivers that do not implement this interface default to priority 0. -/// -/// -/// -/// -/// public class LoggingEventHandler : Attribute, IModuleStartHandler, IEventHandlerPriority -/// { -/// public int Priority => 100; -/// public Task OnModuleStartAsync(IModuleHookContext context) => /* log */; -/// } -/// -/// public class MetricsEventHandler : Attribute, IModuleStartHandler, IEventHandlerPriority -/// { -/// public int Priority => 200; -/// public Task OnModuleStartAsync(IModuleHookContext context) => /* record metrics */; -/// } -/// // LoggingEventHandler (100) runs before MetricsEventHandler (200) -/// -/// -public interface IEventHandlerPriority -{ - /// - /// Gets the execution priority of this event handler. - /// Lower values execute first. Default is 0. - /// - int Priority { get; } -} diff --git a/src/ModularPipelines/Attributes/Events/IModuleEndHandler.cs b/src/ModularPipelines/Attributes/Events/IModuleEndHandler.cs deleted file mode 100644 index 18852279af2..00000000000 --- a/src/ModularPipelines/Attributes/Events/IModuleEndHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -using ModularPipelines.Context; -using ModularPipelines.Models; - -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an attribute to handle module end events. -/// Invoked after a module completes successfully. -/// -public interface IModuleEndHandler -{ - /// - /// Gets a value indicating whether to continue execution if this handler throws an exception. - /// Default is false (propagate exceptions). - /// - bool ContinueOnError => false; - - /// - /// Called when the module has finished executing. - /// - /// The hook context providing module information and control flow. - /// The result of the module execution. - /// A task representing the async operation. - Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result); -} diff --git a/src/ModularPipelines/Attributes/Events/IModuleFailureHandler.cs b/src/ModularPipelines/Attributes/Events/IModuleFailureHandler.cs deleted file mode 100644 index 1c9dea98625..00000000000 --- a/src/ModularPipelines/Attributes/Events/IModuleFailureHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using ModularPipelines.Context; - -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an attribute to handle module failure events. -/// Invoked when a module fails with an exception. -/// Called after Module.OnFailedAsync and before -/// IModuleEventReceiver.OnModuleFailureAsync. -/// -public interface IModuleFailureHandler -{ - /// - /// Gets a value indicating whether to continue execution if this handler throws an exception. - /// Default is false (propagate exceptions). - /// - bool ContinueOnError => false; - - /// - /// Called when the module has failed with an exception. - /// - /// The hook context providing module information and control flow. - /// The exception that caused the failure. - /// A task representing the async operation. - Task OnModuleFailureAsync(IModuleHookContext context, Exception exception); -} diff --git a/src/ModularPipelines/Attributes/Events/IModuleReadyHandler.cs b/src/ModularPipelines/Attributes/Events/IModuleReadyHandler.cs deleted file mode 100644 index 2967b711438..00000000000 --- a/src/ModularPipelines/Attributes/Events/IModuleReadyHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using ModularPipelines.Context; - -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an attribute to handle module ready events. -/// Invoked when all dependencies of a module have completed and the module is ready to execute. -/// This event fires before the module enters the execution queue. -/// -public interface IModuleReadyHandler -{ - /// - /// Gets a value indicating whether to continue execution if this handler throws an exception. - /// Default is false (propagate exceptions). - /// - bool ContinueOnError => false; - - /// - /// Called when all dependencies of the module have completed and it is ready to execute. - /// - /// The hook context providing module information and timing data. - /// A task representing the async operation. - Task OnModuleReadyAsync(IModuleHookContext context); -} diff --git a/src/ModularPipelines/Attributes/Events/IModuleRegistrationEventReceiver.cs b/src/ModularPipelines/Attributes/Events/IModuleRegistrationEventReceiver.cs deleted file mode 100644 index b19c4014824..00000000000 --- a/src/ModularPipelines/Attributes/Events/IModuleRegistrationEventReceiver.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an attribute to receive registration events. -/// Invoked during module registration in the DI container. -/// Use for dynamic dependency configuration and service registration. -/// -public interface IModuleRegistrationEventReceiver -{ - /// - /// Called when the module is being registered. - /// - /// The registration context providing access to dependencies and services. - /// A task representing the async operation. - Task OnRegistrationAsync(IModuleRegistrationContext context); -} diff --git a/src/ModularPipelines/Attributes/Events/IModuleSkippedHandler.cs b/src/ModularPipelines/Attributes/Events/IModuleSkippedHandler.cs deleted file mode 100644 index 4e8652884dd..00000000000 --- a/src/ModularPipelines/Attributes/Events/IModuleSkippedHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -using ModularPipelines.Context; -using ModularPipelines.Models; - -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an attribute to handle module skipped events. -/// Invoked when a module is skipped. -/// -public interface IModuleSkippedHandler -{ - /// - /// Gets a value indicating whether to continue execution if this handler throws an exception. - /// Default is false (propagate exceptions). - /// - bool ContinueOnError => false; - - /// - /// Called when the module has been skipped. - /// - /// The hook context providing module information and control flow. - /// The reason the module was skipped. - /// A task representing the async operation. - Task OnModuleSkippedAsync(IModuleHookContext context, SkipDecision reason); -} diff --git a/src/ModularPipelines/Attributes/Events/IModuleStartHandler.cs b/src/ModularPipelines/Attributes/Events/IModuleStartHandler.cs deleted file mode 100644 index 03a8a1a05ec..00000000000 --- a/src/ModularPipelines/Attributes/Events/IModuleStartHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ModularPipelines.Context; - -namespace ModularPipelines.Attributes.Events; - -/// -/// Implement this interface on an attribute to handle module start events. -/// Invoked immediately before a module starts executing. -/// -public interface IModuleStartHandler -{ - /// - /// Gets a value indicating whether to continue execution if this handler throws an exception. - /// Default is false (propagate exceptions). - /// - bool ContinueOnError => false; - - /// - /// Called when the module is about to start executing. - /// - /// The hook context providing module information and control flow. - /// A task representing the async operation. - Task OnModuleStartAsync(IModuleHookContext context); -} diff --git a/src/ModularPipelines/Attributes/Events/IPlanningSafeModuleRegistrationEventReceiver.cs b/src/ModularPipelines/Attributes/Events/IPlanningSafeModuleRegistrationEventReceiver.cs deleted file mode 100644 index 5e1e000f195..00000000000 --- a/src/ModularPipelines/Attributes/Events/IPlanningSafeModuleRegistrationEventReceiver.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ModularPipelines.Attributes.Events; - -/// -/// Marks a module registration receiver as safe to invoke during dependency graph planning. -/// -/// -/// Planning occurs before pipeline startup and the receiver is invoked again during execution. -/// Implementations must therefore be deterministic, idempotent, and free of external side effects. -/// -public interface IPlanningSafeModuleRegistrationEventReceiver : IModuleRegistrationEventReceiver; diff --git a/src/ModularPipelines/Context/ModuleRegistrationContext.cs b/src/ModularPipelines/Context/ModuleRegistrationContext.cs index 7237e046270..8c2f99645ab 100644 --- a/src/ModularPipelines/Context/ModuleRegistrationContext.cs +++ b/src/ModularPipelines/Context/ModuleRegistrationContext.cs @@ -2,8 +2,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using ModularPipelines.Attributes.Events; using ModularPipelines.Engine.Dependencies; +using ModularPipelines.Events; using ModularPipelines.Modules; namespace ModularPipelines.Context; diff --git a/src/ModularPipelines/DependencyInjection/DependencyInjectionSetup.cs b/src/ModularPipelines/DependencyInjection/DependencyInjectionSetup.cs index b156d33189d..df27ab77a5d 100644 --- a/src/ModularPipelines/DependencyInjection/DependencyInjectionSetup.cs +++ b/src/ModularPipelines/DependencyInjection/DependencyInjectionSetup.cs @@ -339,7 +339,7 @@ private static void RegisterAttributeEventServices(IServiceCollection services) // IDependencyContext is implemented by IModuleMetadataRegistry - expose it for dependency resolution .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton(); } diff --git a/src/ModularPipelines/Engine/Attributes/AttributeEventInvoker.cs b/src/ModularPipelines/Engine/Attributes/AttributeEventInvoker.cs deleted file mode 100644 index c2469257188..00000000000 --- a/src/ModularPipelines/Engine/Attributes/AttributeEventInvoker.cs +++ /dev/null @@ -1,160 +0,0 @@ -using Microsoft.Extensions.Logging; -using ModularPipelines.Attributes.Events; -using ModularPipelines.Context; -using ModularPipelines.Models; - -namespace ModularPipelines.Engine.Attributes; - -/// -/// Invokes attribute event handlers with configurable error handling. -/// -internal class AttributeEventInvoker : IAttributeEventInvoker -{ - private readonly ILogger _logger; - - public AttributeEventInvoker(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeRegistrationReceiversAsync( - IEnumerable receivers, - IModuleRegistrationContext context) - { - foreach (var receiver in receivers) - { - try - { - await receiver.OnRegistrationAsync(context).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Registration receiver {Type} failed", receiver.GetType().Name); - throw; - } - } - } - - public async Task InvokeReadyHandlersAsync( - IEnumerable handlers, - IModuleHookContext context) - { - foreach (var handler in handlers) - { - try - { - await handler.OnModuleReadyAsync(context).ConfigureAwait(false); - } - catch (Exception ex) - { - if (handler.ContinueOnError) - { - _logger.LogWarning(ex, "Ready handler {Type} failed, continuing", handler.GetType().Name); - } - else - { - throw; - } - } - } - } - - public async Task InvokeStartHandlersAsync( - IEnumerable handlers, - IModuleHookContext context) - { - foreach (var handler in handlers) - { - try - { - await handler.OnModuleStartAsync(context).ConfigureAwait(false); - } - catch (Exception ex) - { - if (handler.ContinueOnError) - { - _logger.LogWarning(ex, "Start handler {Type} failed, continuing", handler.GetType().Name); - } - else - { - throw; - } - } - } - } - - public async Task InvokeEndHandlersAsync( - IEnumerable handlers, - IModuleHookContext context, - IModuleResult result) - { - foreach (var handler in handlers) - { - try - { - await handler.OnModuleEndAsync(context, result).ConfigureAwait(false); - } - catch (Exception ex) - { - if (handler.ContinueOnError) - { - _logger.LogWarning(ex, "End handler {Type} failed, continuing", handler.GetType().Name); - } - else - { - throw; - } - } - } - } - - public async Task InvokeFailureHandlersAsync( - IEnumerable handlers, - IModuleHookContext context, - Exception exception) - { - foreach (var handler in handlers) - { - try - { - await handler.OnModuleFailureAsync(context, exception).ConfigureAwait(false); - } - catch (Exception ex) - { - if (handler.ContinueOnError) - { - _logger.LogWarning(ex, "Failure handler {Type} failed, continuing", handler.GetType().Name); - } - else - { - throw; - } - } - } - } - - public async Task InvokeSkippedHandlersAsync( - IEnumerable handlers, - IModuleHookContext context, - SkipDecision reason) - { - foreach (var handler in handlers) - { - try - { - await handler.OnModuleSkippedAsync(context, reason).ConfigureAwait(false); - } - catch (Exception ex) - { - if (handler.ContinueOnError) - { - _logger.LogWarning(ex, "Skipped handler {Type} failed, continuing", handler.GetType().Name); - } - else - { - throw; - } - } - } - } -} diff --git a/src/ModularPipelines/Engine/Attributes/EventHandlerInvoker.cs b/src/ModularPipelines/Engine/Attributes/EventHandlerInvoker.cs new file mode 100644 index 00000000000..5078d689857 --- /dev/null +++ b/src/ModularPipelines/Engine/Attributes/EventHandlerInvoker.cs @@ -0,0 +1,111 @@ +using System.Runtime.ExceptionServices; +using Microsoft.Extensions.Logging; +using ModularPipelines.Context; +using ModularPipelines.Events; +using ModularPipelines.Models; + +namespace ModularPipelines.Engine.Attributes; + +/// +/// Invokes event handlers with configurable error handling. +/// +internal class EventHandlerInvoker : IEventHandlerInvoker +{ + private readonly ILogger _logger; + + public EventHandlerInvoker(ILogger logger) + { + _logger = logger; + } + + public Task InvokePipelineStartHandlersAsync( + IEnumerable handlers, + IPipelineContext context) => + InvokeHandlersAsync(handlers, handler => handler.OnPipelineStartAsync(context), "Pipeline start"); + + public Task InvokePipelineEndHandlersAsync( + IEnumerable handlers, + IPipelineContext context, + PipelineSummary summary) => + InvokeHandlersAsync(handlers, handler => handler.OnPipelineEndAsync(context, summary), "Pipeline end"); + + public Task InvokeRegistrationHandlersAsync( + IEnumerable handlers, + IModuleRegistrationContext context) => + InvokeHandlersAsync(handlers, handler => handler.OnRegistrationAsync(context), "Registration"); + + public Task InvokeReadyHandlersAsync( + IEnumerable handlers, + IModuleHookContext context) => + InvokeHandlersAsync(handlers, handler => handler.OnModuleReadyAsync(context), "Ready"); + + public Task InvokeStartHandlersAsync( + IEnumerable handlers, + IModuleHookContext context) => + InvokeHandlersAsync(handlers, handler => handler.OnModuleStartAsync(context), "Start"); + + public Task InvokeEndHandlersAsync( + IEnumerable handlers, + IModuleHookContext context, + IModuleResult result) => + InvokeHandlersAsync(handlers, handler => handler.OnModuleEndAsync(context, result), "End"); + + public Task InvokeFailureHandlersAsync( + IEnumerable handlers, + IModuleHookContext context, + Exception exception) => + InvokeHandlersAsync(handlers, handler => handler.OnModuleFailureAsync(context, exception), "Failure"); + + public Task InvokeSkippedHandlersAsync( + IEnumerable handlers, + IModuleHookContext context, + SkipDecision reason) => + InvokeHandlersAsync(handlers, handler => handler.OnModuleSkippedAsync(context, reason), "Skipped"); + + private async Task InvokeHandlersAsync( + IEnumerable handlers, + Func invoke, + string eventName) + where THandler : IEventHandler + { + List? failures = null; + + foreach (var handler in handlers) + { + try + { + await invoke(handler).ConfigureAwait(false); + } + catch (Exception ex) + { + if (handler.ContinueOnError) + { + _logger.LogWarning( + ex, + "{EventName} handler {Type} failed, continuing", + eventName, + handler.GetType().Name); + } + else + { + _logger.LogError( + ex, + "{EventName} handler {Type} failed", + eventName, + handler.GetType().Name); + (failures ??= []).Add(ex); + } + } + } + + if (failures is [var failure]) + { + ExceptionDispatchInfo.Capture(failure).Throw(); + } + + if (failures is { Count: > 1 }) + { + throw new AggregateException($"Multiple {eventName} handlers failed.", failures); + } + } +} diff --git a/src/ModularPipelines/Engine/Attributes/IAttributeEventInvoker.cs b/src/ModularPipelines/Engine/Attributes/IEventHandlerInvoker.cs similarity index 58% rename from src/ModularPipelines/Engine/Attributes/IAttributeEventInvoker.cs rename to src/ModularPipelines/Engine/Attributes/IEventHandlerInvoker.cs index febb7384749..cc01aa18f7f 100644 --- a/src/ModularPipelines/Engine/Attributes/IAttributeEventInvoker.cs +++ b/src/ModularPipelines/Engine/Attributes/IEventHandlerInvoker.cs @@ -1,15 +1,22 @@ -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; +using ModularPipelines.Events; using ModularPipelines.Models; namespace ModularPipelines.Engine.Attributes; /// -/// Invokes attribute event handlers with error handling. +/// Invokes event handlers with consistent error handling. /// -internal interface IAttributeEventInvoker +internal interface IEventHandlerInvoker { - Task InvokeRegistrationReceiversAsync(IEnumerable receivers, IModuleRegistrationContext context); + Task InvokePipelineStartHandlersAsync(IEnumerable handlers, IPipelineContext context); + + Task InvokePipelineEndHandlersAsync( + IEnumerable handlers, + IPipelineContext context, + PipelineSummary summary); + + Task InvokeRegistrationHandlersAsync(IEnumerable handlers, IModuleRegistrationContext context); Task InvokeReadyHandlersAsync(IEnumerable handlers, IModuleHookContext context); diff --git a/src/ModularPipelines/Engine/Attributes/IModuleAttributeEventService.cs b/src/ModularPipelines/Engine/Attributes/IModuleAttributeEventService.cs index 3d0eaf4fe4d..4a149e3f3a7 100644 --- a/src/ModularPipelines/Engine/Attributes/IModuleAttributeEventService.cs +++ b/src/ModularPipelines/Engine/Attributes/IModuleAttributeEventService.cs @@ -1,4 +1,4 @@ -using ModularPipelines.Attributes.Events; +using ModularPipelines.Events; namespace ModularPipelines.Engine.Attributes; @@ -9,9 +9,9 @@ internal interface IModuleAttributeEventService { IReadOnlyList GetAttributes(Type moduleType); - IReadOnlyList GetRegistrationReceivers(Type moduleType); + IReadOnlyList GetRegistrationHandlers(Type moduleType); - IReadOnlyList GetPlanningRegistrationReceivers(Type moduleType); + IReadOnlyList GetPlanningRegistrationHandlers(Type moduleType); IReadOnlyList GetPlanningAttributes(Type moduleType); diff --git a/src/ModularPipelines/Engine/Attributes/IRegistrationEventExecutor.cs b/src/ModularPipelines/Engine/Attributes/IRegistrationEventExecutor.cs index ba6fb9bf962..015ebb3dea6 100644 --- a/src/ModularPipelines/Engine/Attributes/IRegistrationEventExecutor.cs +++ b/src/ModularPipelines/Engine/Attributes/IRegistrationEventExecutor.cs @@ -8,7 +8,7 @@ namespace ModularPipelines.Engine.Attributes; internal interface IRegistrationEventExecutor { /// - /// Invokes registration event receivers for all modules. + /// Invokes registration event handlers for all modules. /// This should be called before dependency resolution. /// Task InvokeRegistrationEventsAsync(IEnumerable modules); diff --git a/src/ModularPipelines/Engine/Attributes/ModuleAttributeEventService.cs b/src/ModularPipelines/Engine/Attributes/ModuleAttributeEventService.cs index 305457e5a69..953078364dd 100644 --- a/src/ModularPipelines/Engine/Attributes/ModuleAttributeEventService.cs +++ b/src/ModularPipelines/Engine/Attributes/ModuleAttributeEventService.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; -using ModularPipelines.Attributes.Events; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Generated; @@ -12,8 +12,7 @@ namespace ModularPipelines.Engine.Attributes; /// /// Discovers and caches attribute event handlers on modules. /// Handlers are returned sorted by priority (lower values first). -/// Handlers that implement are sorted by their value. -/// Handlers without priority default to 0. +/// The default is 0. /// internal class ModuleAttributeEventService : IModuleAttributeEventService { @@ -23,11 +22,11 @@ internal class ModuleAttributeEventService : IModuleAttributeEventService public IReadOnlyList GetAttributes(Type moduleType) => GetCache(moduleType).Attributes; - public IReadOnlyList GetRegistrationReceivers(Type moduleType) - => GetCache(moduleType).RegistrationReceivers; + public IReadOnlyList GetRegistrationHandlers(Type moduleType) + => GetCache(moduleType).RegistrationHandlers; - public IReadOnlyList GetPlanningRegistrationReceivers(Type moduleType) - => GetPlanningCache(moduleType).RegistrationReceivers; + public IReadOnlyList GetPlanningRegistrationHandlers(Type moduleType) + => GetPlanningCache(moduleType).RegistrationHandlers; public IReadOnlyList GetPlanningAttributes(Type moduleType) => GetPlanningCache(moduleType).Attributes; @@ -41,33 +40,33 @@ private PlanningAttributeCache GetPlanningCache(Type moduleType) private static PlanningAttributeCache DiscoverPlanningAttributes(Type moduleType) { - var receiverData = CustomAttributeMetadata.GetApplicable( + var handlerData = CustomAttributeMetadata.GetApplicable( moduleType, - static type => typeof(IModuleRegistrationEventReceiver).IsAssignableFrom(type)); - if (receiverData.Count == 0) + static type => typeof(IModuleRegistrationHandler).IsAssignableFrom(type)); + if (handlerData.Count == 0) { return new PlanningAttributeCache([], []); } - var deferredReceiverTypes = receiverData + var unsafeHandlerTypes = handlerData .Select(static data => data.AttributeType) - .Where(static type => !typeof(IPlanningSafeModuleRegistrationEventReceiver).IsAssignableFrom(type)) .Distinct() + .Where(static type => !typeof(IPlanningSafeModuleRegistrationHandler).IsAssignableFrom(type)) .ToArray(); - if (deferredReceiverTypes.Length > 0) + if (unsafeHandlerTypes.Length > 0) { throw new PipelineException( $"Cannot export a resolved dependency graph because {moduleType.FullName} has " - + "registration receivers that are not planning-safe: " - + string.Join(", ", deferredReceiverTypes.Select(static type => type.FullName)) - + $". Implement {nameof(IPlanningSafeModuleRegistrationEventReceiver)} only when " - + "the receiver is deterministic, idempotent, and free of external side effects."); + + "registration handlers that are not planning-safe: " + + string.Join(", ", unsafeHandlerTypes.Select(static type => type.FullName)) + + $". Implement {nameof(IPlanningSafeModuleRegistrationHandler)} only when " + + "the handler is deterministic, idempotent, and free of external side effects."); } var attributeData = CustomAttributeMetadata.GetApplicable(moduleType, static _ => true); var attributes = attributeData.Select(CreatePlanningAttribute).ToArray(); - var receivers = attributes.OfType().ToList(); - return new PlanningAttributeCache(attributes, SortByPriority(receivers)); + var handlers = attributes.OfType().ToList(); + return new PlanningAttributeCache(attributes, SortByPriority(handlers)); } [UnconditionalSuppressMessage( @@ -76,7 +75,7 @@ private static PlanningAttributeCache DiscoverPlanningAttributes(Type moduleType Justification = "The exact attribute type is preserved by the custom-attribute metadata being inspected.")] private static Attribute CreatePlanningAttribute(CustomAttributeData data) { - if (typeof(IPlanningSafeModuleRegistrationEventReceiver).IsAssignableFrom(data.AttributeType) + if (typeof(IModuleRegistrationHandler).IsAssignableFrom(data.AttributeType) || data.AttributeType.Assembly == typeof(ModuleAttributeEventService).Assembly) { return CustomAttributeMetadata.Create(data); @@ -89,9 +88,9 @@ private static Attribute CreatePlanningAttribute(CustomAttributeData data) { throw new PipelineException( $"Cannot export a resolved dependency graph because {data.AttributeType.FullName} is a stateful " - + "companion to a planning-safe registration receiver. Planning cannot construct arbitrary " + + "companion to a planning-safe registration handler. Planning cannot construct arbitrary " + "companion attributes. Use a stateless marker attribute or move the required state onto the " - + $"{nameof(IPlanningSafeModuleRegistrationEventReceiver)} attribute."); + + $"{nameof(IModuleRegistrationHandler)} attribute."); } return (Attribute) RuntimeHelpers.GetUninitializedObject(data.AttributeType); @@ -164,7 +163,7 @@ private static IReadOnlyList CreateAttributes(Type moduleType) private static AttributeHandlerCache CreateHandlerCache(IReadOnlyList attributes) { - var registrationReceivers = new List(); + var registrationHandlers = new List(); var readyHandlers = new List(); var startHandlers = new List(); var endHandlers = new List(); @@ -173,9 +172,9 @@ private static AttributeHandlerCache CreateHandlerCache(IReadOnlyList foreach (var attribute in attributes) { - if (attribute is IModuleRegistrationEventReceiver registration) + if (attribute is IModuleRegistrationHandler registrationHandler) { - registrationReceivers.Add(registration); + registrationHandlers.Add(registrationHandler); } if (attribute is IModuleReadyHandler ready) @@ -204,11 +203,9 @@ private static AttributeHandlerCache CreateHandlerCache(IReadOnlyList } } - // Sort all handlers by priority (lower values first) - // Handlers without IEventHandlerPriority default to 0 return new AttributeHandlerCache( attributes, - SortByPriority(registrationReceivers), + SortByPriority(registrationHandlers), SortByPriority(readyHandlers), SortByPriority(startHandlers), SortByPriority(endHandlers), @@ -229,6 +226,7 @@ private static IReadOnlyList DiscoverAttributesWithReflection(Type mo => [.. moduleType.GetCustomAttributes(inherit: true).OfType()]; private static IReadOnlyList SortByPriority(List handlers) + where T : IEventHandler { if (handlers.Count <= 1) { @@ -236,15 +234,12 @@ private static IReadOnlyList SortByPriority(List handlers) } // Use stable sort to preserve declaration order for handlers with same priority - return [.. handlers.OrderBy(GetPriority)]; + return [.. handlers.OrderBy(static handler => handler.Priority)]; } - private static int GetPriority(T handler) - => handler is IEventHandlerPriority prioritized ? prioritized.Priority : 0; - private sealed record AttributeHandlerCache( IReadOnlyList Attributes, - IReadOnlyList RegistrationReceivers, + IReadOnlyList RegistrationHandlers, IReadOnlyList ReadyHandlers, IReadOnlyList StartHandlers, IReadOnlyList EndHandlers, @@ -253,5 +248,5 @@ private sealed record AttributeHandlerCache( private sealed record PlanningAttributeCache( IReadOnlyList Attributes, - IReadOnlyList RegistrationReceivers); + IReadOnlyList RegistrationHandlers); } diff --git a/src/ModularPipelines/Engine/Attributes/RegistrationEventExecutor.cs b/src/ModularPipelines/Engine/Attributes/RegistrationEventExecutor.cs index 432b21a496f..709655a8780 100644 --- a/src/ModularPipelines/Engine/Attributes/RegistrationEventExecutor.cs +++ b/src/ModularPipelines/Engine/Attributes/RegistrationEventExecutor.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; using ModularPipelines.Engine.Dependencies; +using ModularPipelines.Events; using ModularPipelines.Modules; namespace ModularPipelines.Engine.Attributes; @@ -14,7 +14,7 @@ internal class RegistrationEventExecutor : IRegistrationEventExecutor { private readonly object _lock = new(); private readonly IModuleAttributeEventService _attributeEventService; - private readonly IAttributeEventInvoker _attributeEventInvoker; + private readonly IEventHandlerInvoker _eventHandlerInvoker; private readonly IModuleDependencyRegistry _dependencyRegistry; private readonly IModuleMetadataRegistry _metadataRegistry; private readonly IConfiguration _configuration; @@ -25,14 +25,14 @@ internal class RegistrationEventExecutor : IRegistrationEventExecutor public RegistrationEventExecutor( IModuleAttributeEventService attributeEventService, - IAttributeEventInvoker attributeEventInvoker, + IEventHandlerInvoker eventHandlerInvoker, IModuleDependencyRegistry dependencyRegistry, IModuleMetadataRegistry metadataRegistry, IConfiguration configuration, IHostEnvironment environment) : this( attributeEventService, - attributeEventInvoker, + eventHandlerInvoker, dependencyRegistry, metadataRegistry, configuration, @@ -43,7 +43,7 @@ public RegistrationEventExecutor( internal RegistrationEventExecutor( IModuleAttributeEventService attributeEventService, - IAttributeEventInvoker attributeEventInvoker, + IEventHandlerInvoker eventHandlerInvoker, IModuleDependencyRegistry dependencyRegistry, IModuleMetadataRegistry metadataRegistry, IConfiguration configuration, @@ -51,7 +51,7 @@ internal RegistrationEventExecutor( bool planningSafeOnly) { _attributeEventService = attributeEventService; - _attributeEventInvoker = attributeEventInvoker; + _eventHandlerInvoker = eventHandlerInvoker; _dependencyRegistry = dependencyRegistry; _metadataRegistry = metadataRegistry; _configuration = configuration; @@ -83,7 +83,7 @@ public Task InvokeRegistrationEventsAsync(IEnumerable modules) } // Discovery invokes registration events early so dynamic dependencies can affect filtering. - // Executors invoke this service again, so share the first invocation rather than running receivers twice. + // Executors invoke this service again, so share the first invocation rather than running handlers twice. _registeredModuleTypes = moduleArray .Select(module => module.GetType()) .ToHashSet(); @@ -98,9 +98,9 @@ private async Task InvokeRegistrationEventsInternalAsync(IReadOnlyList foreach (var module in modules) { var moduleType = module.GetType(); - var receivers = GetRegistrationReceivers(moduleType); + var handlers = GetRegistrationHandlers(moduleType); - if (receivers.Length == 0) + if (handlers.Length == 0) { continue; } @@ -116,15 +116,15 @@ private async Task InvokeRegistrationEventsInternalAsync(IReadOnlyList _dependencyRegistry, _metadataRegistry); - await _attributeEventInvoker.InvokeRegistrationReceiversAsync(receivers, context).ConfigureAwait(false); + await _eventHandlerInvoker.InvokeRegistrationHandlersAsync(handlers, context).ConfigureAwait(false); } } - private IModuleRegistrationEventReceiver[] GetRegistrationReceivers(Type moduleType) + private IModuleRegistrationHandler[] GetRegistrationHandlers(Type moduleType) { - var receivers = _planningSafeOnly - ? _attributeEventService.GetPlanningRegistrationReceivers(moduleType) - : _attributeEventService.GetRegistrationReceivers(moduleType); - return [.. receivers]; + var handlers = _planningSafeOnly + ? _attributeEventService.GetPlanningRegistrationHandlers(moduleType) + : _attributeEventService.GetRegistrationHandlers(moduleType); + return [.. handlers]; } } diff --git a/src/ModularPipelines/Engine/Dependencies/ModuleDependencyRegistry.cs b/src/ModularPipelines/Engine/Dependencies/ModuleDependencyRegistry.cs index 67a442c320f..61f744bbe48 100644 --- a/src/ModularPipelines/Engine/Dependencies/ModuleDependencyRegistry.cs +++ b/src/ModularPipelines/Engine/Dependencies/ModuleDependencyRegistry.cs @@ -1,7 +1,7 @@ namespace ModularPipelines.Engine.Dependencies; /// -/// Stores dynamic dependencies added via attribute event receivers. +/// Stores dynamic dependencies added via attribute event handlers. /// /// /// diff --git a/src/ModularPipelines/Engine/Execution/IModuleLifecycleEventInvoker.cs b/src/ModularPipelines/Engine/Execution/IModuleLifecycleEventInvoker.cs index 8ce5eb1e66f..3f48c2d7c64 100644 --- a/src/ModularPipelines/Engine/Execution/IModuleLifecycleEventInvoker.cs +++ b/src/ModularPipelines/Engine/Execution/IModuleLifecycleEventInvoker.cs @@ -29,7 +29,7 @@ internal interface IModuleLifecycleEventInvoker /// Invokes the OnModuleFailed lifecycle event. /// Called when a module throws an exception. /// - Task InvokeFailedEventAsync(ModuleLifecycleContext context, Exception exception); + Task InvokeFailedEventAsync(ModuleLifecycleContext context, IModuleResult result, Exception exception); /// /// Invokes the OnModuleSkipped lifecycle event. diff --git a/src/ModularPipelines/Engine/Execution/ModuleLifecycleEventInvoker.cs b/src/ModularPipelines/Engine/Execution/ModuleLifecycleEventInvoker.cs index 959c2f5c174..ca595ebd10e 100644 --- a/src/ModularPipelines/Engine/Execution/ModuleLifecycleEventInvoker.cs +++ b/src/ModularPipelines/Engine/Execution/ModuleLifecycleEventInvoker.cs @@ -11,16 +11,16 @@ namespace ModularPipelines.Engine.Execution; internal class ModuleLifecycleEventInvoker : IModuleLifecycleEventInvoker { private readonly IModuleAttributeEventService _attributeEventService; - private readonly IAttributeEventInvoker _attributeEventInvoker; + private readonly IEventHandlerInvoker _eventHandlerInvoker; private readonly IModuleMetadataRegistry _metadataRegistry; public ModuleLifecycleEventInvoker( IModuleAttributeEventService attributeEventService, - IAttributeEventInvoker attributeEventInvoker, + IEventHandlerInvoker eventHandlerInvoker, IModuleMetadataRegistry metadataRegistry) { _attributeEventService = attributeEventService; - _attributeEventInvoker = attributeEventInvoker; + _eventHandlerInvoker = eventHandlerInvoker; _metadataRegistry = metadataRegistry; } @@ -43,7 +43,7 @@ public async Task InvokeReadyEventAsync(ModuleLifecycleContext context) context.PipelineContext, _metadataRegistry); - await _attributeEventInvoker.InvokeReadyHandlersAsync(handlers, hookContext).ConfigureAwait(false); + await _eventHandlerInvoker.InvokeReadyHandlersAsync(handlers, hookContext).ConfigureAwait(false); } /// @@ -63,7 +63,7 @@ public async Task InvokeStartEventAsync(ModuleLifecycleContext context) context.PipelineContext, _metadataRegistry); - await _attributeEventInvoker.InvokeStartHandlersAsync(handlers, hookContext).ConfigureAwait(false); + await _eventHandlerInvoker.InvokeStartHandlersAsync(handlers, hookContext).ConfigureAwait(false); } /// @@ -83,11 +83,14 @@ public async Task InvokeEndEventAsync(ModuleLifecycleContext context, Enums.Modu context.PipelineContext, _metadataRegistry); - await _attributeEventInvoker.InvokeEndHandlersAsync(handlers, hookContext, result).ConfigureAwait(false); + await _eventHandlerInvoker.InvokeEndHandlersAsync(handlers, hookContext, result).ConfigureAwait(false); } /// - public async Task InvokeFailedEventAsync(ModuleLifecycleContext context, Exception exception) + public async Task InvokeFailedEventAsync( + ModuleLifecycleContext context, + IModuleResult result, + Exception exception) { var handlers = _attributeEventService.GetFailureHandlers(context.ModuleType); if (handlers.Count == 0) @@ -99,11 +102,11 @@ public async Task InvokeFailedEventAsync(ModuleLifecycleContext context, Excepti context.Module, context.ModuleAttributes, context.StartTime, - result: null, + result, context.PipelineContext, _metadataRegistry); - await _attributeEventInvoker.InvokeFailureHandlersAsync(handlers, hookContext, exception).ConfigureAwait(false); + await _eventHandlerInvoker.InvokeFailureHandlersAsync(handlers, hookContext, exception).ConfigureAwait(false); } /// @@ -123,6 +126,6 @@ public async Task InvokeSkippedEventAsync(ModuleLifecycleContext context, Enums. context.PipelineContext, _metadataRegistry); - await _attributeEventInvoker.InvokeSkippedHandlersAsync(handlers, hookContext, skipReason).ConfigureAwait(false); + await _eventHandlerInvoker.InvokeSkippedHandlersAsync(handlers, hookContext, skipReason).ConfigureAwait(false); } } diff --git a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs index ee67bcd3249..23c7ec4d510 100644 --- a/src/ModularPipelines/Engine/Execution/ModuleRunner.cs +++ b/src/ModularPipelines/Engine/Execution/ModuleRunner.cs @@ -1042,8 +1042,8 @@ or Enums.ModuleStatus.NotStarted try { - await _lifecycleEventInvoker.InvokeFailedEventAsync(lifecycleContext, exception).ConfigureAwait(false); - await _pipelineSetupExecutor.OnModuleFailureAsync(moduleState).ConfigureAwait(false); + await _lifecycleEventInvoker.InvokeFailedEventAsync(lifecycleContext, result, exception).ConfigureAwait(false); + await _pipelineSetupExecutor.OnModuleFailureAsync(moduleState, exception).ConfigureAwait(false); } finally { @@ -1068,7 +1068,7 @@ await _lifecycleEventInvoker.InvokeSkippedEventAsync( Enums.ModuleStatus.Skipped, executionContext.SkipResult!) .ConfigureAwait(false); - await _pipelineSetupExecutor.OnModuleSkippedAsync(moduleState).ConfigureAwait(false); + await _pipelineSetupExecutor.OnModuleSkippedAsync(moduleState, executionContext.SkipResult!).ConfigureAwait(false); return; } @@ -1080,7 +1080,7 @@ await _moduleEstimatedTimeProvider.SaveModuleTimeAsync( .ConfigureAwait(false); } - await _pipelineSetupExecutor.OnModuleEndAsync(moduleState).ConfigureAwait(false); + await _pipelineSetupExecutor.OnModuleEndAsync(moduleState, result).ConfigureAwait(false); await _lifecycleEventInvoker.InvokeEndEventAsync(lifecycleContext, executionContext.Status, result).ConfigureAwait(false); if (!_manageArtifactsLocally diff --git a/src/ModularPipelines/Engine/IPipelineSetupExecutor.cs b/src/ModularPipelines/Engine/IPipelineSetupExecutor.cs index 67ca9c06014..192a361b4a2 100644 --- a/src/ModularPipelines/Engine/IPipelineSetupExecutor.cs +++ b/src/ModularPipelines/Engine/IPipelineSetupExecutor.cs @@ -12,9 +12,9 @@ internal interface IPipelineSetupExecutor Task OnModuleStartAsync(ModuleState moduleState); - Task OnModuleEndAsync(ModuleState moduleState); + Task OnModuleEndAsync(ModuleState moduleState, IModuleResult result); - Task OnModuleFailureAsync(ModuleState moduleState); + Task OnModuleFailureAsync(ModuleState moduleState, Exception exception); - Task OnModuleSkippedAsync(ModuleState moduleState); + Task OnModuleSkippedAsync(ModuleState moduleState, SkipDecision reason); } diff --git a/src/ModularPipelines/Engine/ModuleDiscoveryPlanner.cs b/src/ModularPipelines/Engine/ModuleDiscoveryPlanner.cs index 8797b7cc7ea..441a6ca6188 100644 --- a/src/ModularPipelines/Engine/ModuleDiscoveryPlanner.cs +++ b/src/ModularPipelines/Engine/ModuleDiscoveryPlanner.cs @@ -20,7 +20,7 @@ namespace ModularPipelines.Engine; internal sealed class ModuleDiscoveryPlanner( IModuleConditionHandler moduleConditionHandler, - IAttributeEventInvoker attributeEventInvoker, + IEventHandlerInvoker eventHandlerInvoker, IConfiguration configuration, IHostEnvironment environment, IEnumerable modules, @@ -92,7 +92,7 @@ public async Task DiscoverAsync( planningSafeOnly: true); var registrationEventExecutor = new RegistrationEventExecutor( attributeEventService, - attributeEventInvoker, + eventHandlerInvoker, planningDependencyRegistry, planningMetadataRegistry, configuration, diff --git a/src/ModularPipelines/Engine/ModuleExecutor.cs b/src/ModularPipelines/Engine/ModuleExecutor.cs index 5bdb8dd3e30..d020a934c7c 100644 --- a/src/ModularPipelines/Engine/ModuleExecutor.cs +++ b/src/ModularPipelines/Engine/ModuleExecutor.cs @@ -5,6 +5,7 @@ using ModularPipelines.Engine.Attributes; using ModularPipelines.Engine.Dependencies; using ModularPipelines.Engine.Execution; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Helpers; using ModularPipelines.Interfaces; diff --git a/src/ModularPipelines/Engine/PipelineSetupExecutor.cs b/src/ModularPipelines/Engine/PipelineSetupExecutor.cs index d4b34a66a8c..01753e83192 100644 --- a/src/ModularPipelines/Engine/PipelineSetupExecutor.cs +++ b/src/ModularPipelines/Engine/PipelineSetupExecutor.cs @@ -1,28 +1,31 @@ using ModularPipelines.Context; using ModularPipelines.Engine.Attributes; using ModularPipelines.Engine.Dependencies; -using ModularPipelines.Interfaces; +using ModularPipelines.Events; using ModularPipelines.Models; namespace ModularPipelines.Engine; internal class PipelineSetupExecutor : IPipelineSetupExecutor { - private readonly IEnumerable _globalHooks; - private readonly IReadOnlyCollection _moduleEventReceivers; + private readonly IReadOnlyList _pipelineEventHandlers; + private readonly IReadOnlyList _moduleEventHandlers; + private readonly IEventHandlerInvoker _eventHandlerInvoker; private readonly IPipelineContextProvider _moduleContextProvider; private readonly IModuleMetadataRegistry _metadataRegistry; private readonly IModuleAttributeEventService _attributeEventService; - public PipelineSetupExecutor(IEnumerable globalHooks, - IEnumerable moduleEventReceivers, + public PipelineSetupExecutor( + IEnumerable pipelineEventHandlers, + IEnumerable moduleEventHandlers, + IEventHandlerInvoker eventHandlerInvoker, IPipelineContextProvider moduleContextProvider, IModuleMetadataRegistry metadataRegistry, IModuleAttributeEventService attributeEventService) { - _globalHooks = globalHooks; - _moduleEventReceivers = moduleEventReceivers as IReadOnlyCollection - ?? moduleEventReceivers.ToArray(); + _pipelineEventHandlers = [.. pipelineEventHandlers.OrderBy(static handler => handler.Priority)]; + _moduleEventHandlers = [.. moduleEventHandlers.OrderBy(static handler => handler.Priority)]; + _eventHandlerInvoker = eventHandlerInvoker; _moduleContextProvider = moduleContextProvider; _metadataRegistry = metadataRegistry; _attributeEventService = attributeEventService; @@ -30,51 +33,69 @@ public PipelineSetupExecutor(IEnumerable globalHooks, public Task OnPipelineStartAsync() { - return Task.WhenAll(_globalHooks.Select(x => x.OnPipelineStartAsync(GetPipelineContext()))); + return _pipelineEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokePipelineStartHandlersAsync( + _pipelineEventHandlers, + GetPipelineContext()); } public Task OnPipelineEndAsync(PipelineSummary pipelineSummary) { - return Task.WhenAll(_globalHooks.Select(x => x.OnPipelineEndAsync(GetPipelineContext(), pipelineSummary))); + return _pipelineEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokePipelineEndHandlersAsync( + _pipelineEventHandlers, + GetPipelineContext(), + pipelineSummary); } public Task OnModuleReadyAsync(ModuleState moduleState) - => InvokeModuleEventReceiversAsync( - moduleState, - static (receiver, context) => receiver.OnModuleReadyAsync(context)); + { + return _moduleEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokeReadyHandlersAsync( + _moduleEventHandlers, + CreateModuleHookContext(moduleState)); + } public Task OnModuleStartAsync(ModuleState moduleState) - => InvokeModuleEventReceiversAsync( - moduleState, - static (receiver, context) => receiver.OnModuleStartAsync(context)); - - public Task OnModuleEndAsync(ModuleState moduleState) - => InvokeModuleEventReceiversAsync( - moduleState, - static (receiver, context) => receiver.OnModuleEndAsync(context)); - - public Task OnModuleFailureAsync(ModuleState moduleState) - => InvokeModuleEventReceiversAsync( - moduleState, - static (receiver, context) => receiver.OnModuleFailureAsync(context)); + { + return _moduleEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokeStartHandlersAsync( + _moduleEventHandlers, + CreateModuleHookContext(moduleState)); + } - public Task OnModuleSkippedAsync(ModuleState moduleState) - => InvokeModuleEventReceiversAsync( - moduleState, - static (receiver, context) => receiver.OnModuleSkippedAsync(context)); + public Task OnModuleEndAsync(ModuleState moduleState, IModuleResult result) + { + return _moduleEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokeEndHandlersAsync( + _moduleEventHandlers, + CreateModuleHookContext(moduleState), + result); + } - private Task InvokeModuleEventReceiversAsync( - ModuleState moduleState, - Func invokeReceiver) + public Task OnModuleFailureAsync(ModuleState moduleState, Exception exception) { - if (_moduleEventReceivers.Count == 0) - { - return Task.CompletedTask; - } + return _moduleEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokeFailureHandlersAsync( + _moduleEventHandlers, + CreateModuleHookContext(moduleState), + exception); + } - var context = CreateModuleHookContext(moduleState); - return Task.WhenAll( - _moduleEventReceivers.Select(receiver => invokeReceiver(receiver, context))); + public Task OnModuleSkippedAsync(ModuleState moduleState, SkipDecision reason) + { + return _moduleEventHandlers.Count == 0 + ? Task.CompletedTask + : _eventHandlerInvoker.InvokeSkippedHandlersAsync( + _moduleEventHandlers, + CreateModuleHookContext(moduleState), + reason); } private IPipelineContext GetPipelineContext() diff --git a/src/ModularPipelines/Events/IEventHandler.cs b/src/ModularPipelines/Events/IEventHandler.cs new file mode 100644 index 00000000000..8b62ece1d30 --- /dev/null +++ b/src/ModularPipelines/Events/IEventHandler.cs @@ -0,0 +1,17 @@ +namespace ModularPipelines.Events; + +/// +/// Defines behavior shared by pipeline and module event handlers. +/// +public interface IEventHandler +{ + /// + /// Gets whether execution continues when this handler throws. + /// + bool ContinueOnError => false; + + /// + /// Gets the execution priority. Lower values run first. + /// + int Priority => 0; +} diff --git a/src/ModularPipelines/Events/IModuleEndHandler.cs b/src/ModularPipelines/Events/IModuleEndHandler.cs new file mode 100644 index 00000000000..2921b78791a --- /dev/null +++ b/src/ModularPipelines/Events/IModuleEndHandler.cs @@ -0,0 +1,18 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; + +namespace ModularPipelines.Events; + +/// +/// Handles the event raised after a module completes successfully. +/// +public interface IModuleEndHandler : IEventHandler +{ + /// + /// Called when the module has finished executing. + /// + /// The module hook context. + /// The module execution result. + /// A task representing the asynchronous operation. + Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result); +} diff --git a/src/ModularPipelines/Events/IModuleEventHandler.cs b/src/ModularPipelines/Events/IModuleEventHandler.cs new file mode 100644 index 00000000000..46576328f6a --- /dev/null +++ b/src/ModularPipelines/Events/IModuleEventHandler.cs @@ -0,0 +1,29 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; + +namespace ModularPipelines.Events; + +/// +/// Handles lifecycle events for every module in a pipeline. +/// +/// +/// Register implementations with AddModuleEventHandler<THandler>(). +/// Implement only the callbacks the handler needs; unimplemented callbacks do nothing. +/// +public interface IModuleEventHandler : + IModuleReadyHandler, + IModuleStartHandler, + IModuleEndHandler, + IModuleFailureHandler, + IModuleSkippedHandler +{ + Task IModuleReadyHandler.OnModuleReadyAsync(IModuleHookContext context) => Task.CompletedTask; + + Task IModuleStartHandler.OnModuleStartAsync(IModuleHookContext context) => Task.CompletedTask; + + Task IModuleEndHandler.OnModuleEndAsync(IModuleHookContext context, IModuleResult result) => Task.CompletedTask; + + Task IModuleFailureHandler.OnModuleFailureAsync(IModuleHookContext context, Exception exception) => Task.CompletedTask; + + Task IModuleSkippedHandler.OnModuleSkippedAsync(IModuleHookContext context, SkipDecision reason) => Task.CompletedTask; +} diff --git a/src/ModularPipelines/Events/IModuleFailureHandler.cs b/src/ModularPipelines/Events/IModuleFailureHandler.cs new file mode 100644 index 00000000000..070d2134bd9 --- /dev/null +++ b/src/ModularPipelines/Events/IModuleFailureHandler.cs @@ -0,0 +1,17 @@ +using ModularPipelines.Context; + +namespace ModularPipelines.Events; + +/// +/// Handles the event raised when a module fails. +/// +public interface IModuleFailureHandler : IEventHandler +{ + /// + /// Called when the module fails with an exception. + /// + /// The module hook context. + /// The exception that caused the module to fail. + /// A task representing the asynchronous operation. + Task OnModuleFailureAsync(IModuleHookContext context, Exception exception); +} diff --git a/src/ModularPipelines/Events/IModuleReadyHandler.cs b/src/ModularPipelines/Events/IModuleReadyHandler.cs new file mode 100644 index 00000000000..e1368311b7e --- /dev/null +++ b/src/ModularPipelines/Events/IModuleReadyHandler.cs @@ -0,0 +1,16 @@ +using ModularPipelines.Context; + +namespace ModularPipelines.Events; + +/// +/// Handles the event raised when a module's dependencies are satisfied. +/// +public interface IModuleReadyHandler : IEventHandler +{ + /// + /// Called when the module is ready to execute. + /// + /// The module hook context. + /// A task representing the asynchronous operation. + Task OnModuleReadyAsync(IModuleHookContext context); +} diff --git a/src/ModularPipelines/Attributes/Events/IModuleRegistrationContext.cs b/src/ModularPipelines/Events/IModuleRegistrationContext.cs similarity index 52% rename from src/ModularPipelines/Attributes/Events/IModuleRegistrationContext.cs rename to src/ModularPipelines/Events/IModuleRegistrationContext.cs index ce9dad2e02a..0a452f4529a 100644 --- a/src/ModularPipelines/Attributes/Events/IModuleRegistrationContext.cs +++ b/src/ModularPipelines/Events/IModuleRegistrationContext.cs @@ -3,11 +3,11 @@ using Microsoft.Extensions.Hosting; using ModularPipelines.Modules; -namespace ModularPipelines.Attributes.Events; +namespace ModularPipelines.Events; /// -/// Context provided to . -/// Provides access to pipeline configuration and enables dynamic dependency manipulation. +/// Context provided to . +/// Provides pipeline configuration and dynamic dependency operations. /// public interface IModuleRegistrationContext { @@ -37,74 +37,86 @@ public interface IModuleRegistrationContext IReadOnlyList RegisteredModuleTypes { get; } /// - /// Checks if a specific module type is registered. + /// Checks whether a module of the specified type is registered. /// - /// + /// The module type. + /// when the module is registered; otherwise, . bool IsModuleRegistered() where TModule : IModule; /// - /// Checks if a specific module type is registered. + /// Checks whether a module of the specified type is registered. /// - /// + /// The module type. + /// when the module is registered; otherwise, . bool IsModuleRegistered(Type moduleType); /// - /// Gets all registered module types that are assignable to the specified base type. + /// Gets all registered module types assignable to the specified base type. /// - /// + /// The base module type. + /// The matching registered module types. IEnumerable GetModulesAssignableTo() where TBase : IModule; /// - /// Gets all registered module types that have the specified attribute. + /// Gets all registered module types with the specified attribute. /// - /// + /// The attribute type. + /// The matching registered module types. IEnumerable GetModulesWithAttribute() where TAttribute : Attribute; /// - /// Adds a dependency on another module. + /// Adds a dependency on the specified module. /// + /// The dependency module type. void AddDependency() where TModule : IModule; /// - /// Adds a dependency on another module. + /// Adds a dependency on the specified module. /// + /// The dependency module type. void AddDependency(Type moduleType); /// /// Adds dependencies on all modules assignable to the specified base type. /// + /// The base module type. void AddDependencyOnAll() where TBase : IModule; /// /// Adds dependencies on all modules matching the specified predicate. /// + /// The predicate used to select module types. void AddDependencyOnAll(Func predicate); /// - /// Removes a dependency on another module. + /// Removes a dependency on the specified module. /// + /// The dependency module type. void RemoveDependency() where TModule : IModule; /// - /// Gets the service collection for registering additional services. - /// This is null when the context is used post-container-build (e.g., for hook execution). + /// Gets the service collection when registration occurs before the container is built; otherwise, . /// IServiceCollection? Services { get; } /// /// Sets metadata that can be retrieved during module execution. /// + /// The metadata key. + /// The metadata value. void SetMetadata(string key, object value); /// - /// Gets metadata that was set during registration. + /// Gets metadata set during registration. /// - /// + /// The metadata value type. + /// The metadata key. + /// The metadata value, or the default value when the key is absent. T? GetMetadata(string key); } diff --git a/src/ModularPipelines/Events/IModuleRegistrationHandler.cs b/src/ModularPipelines/Events/IModuleRegistrationHandler.cs new file mode 100644 index 00000000000..326ac124187 --- /dev/null +++ b/src/ModularPipelines/Events/IModuleRegistrationHandler.cs @@ -0,0 +1,14 @@ +namespace ModularPipelines.Events; + +/// +/// Handles module registration for an attribute. +/// +public interface IModuleRegistrationHandler : IEventHandler +{ + /// + /// Called when the module is being registered. + /// + /// The module registration context. + /// A task representing the asynchronous operation. + Task OnRegistrationAsync(IModuleRegistrationContext context); +} diff --git a/src/ModularPipelines/Events/IModuleSkippedHandler.cs b/src/ModularPipelines/Events/IModuleSkippedHandler.cs new file mode 100644 index 00000000000..54b68f8a3c7 --- /dev/null +++ b/src/ModularPipelines/Events/IModuleSkippedHandler.cs @@ -0,0 +1,18 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; + +namespace ModularPipelines.Events; + +/// +/// Handles the event raised when a module is skipped. +/// +public interface IModuleSkippedHandler : IEventHandler +{ + /// + /// Called when the module is skipped. + /// + /// The module hook context. + /// The decision that caused the module to be skipped. + /// A task representing the asynchronous operation. + Task OnModuleSkippedAsync(IModuleHookContext context, SkipDecision reason); +} diff --git a/src/ModularPipelines/Events/IModuleStartHandler.cs b/src/ModularPipelines/Events/IModuleStartHandler.cs new file mode 100644 index 00000000000..9d7915563e8 --- /dev/null +++ b/src/ModularPipelines/Events/IModuleStartHandler.cs @@ -0,0 +1,16 @@ +using ModularPipelines.Context; + +namespace ModularPipelines.Events; + +/// +/// Handles the event raised immediately before a module starts. +/// +public interface IModuleStartHandler : IEventHandler +{ + /// + /// Called when the module is about to start executing. + /// + /// The module hook context. + /// A task representing the asynchronous operation. + Task OnModuleStartAsync(IModuleHookContext context); +} diff --git a/src/ModularPipelines/Events/IPipelineEventHandler.cs b/src/ModularPipelines/Events/IPipelineEventHandler.cs new file mode 100644 index 00000000000..a01105b4738 --- /dev/null +++ b/src/ModularPipelines/Events/IPipelineEventHandler.cs @@ -0,0 +1,25 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; + +namespace ModularPipelines.Events; + +/// +/// Handles pipeline-level lifecycle events. +/// +public interface IPipelineEventHandler : IEventHandler +{ + /// + /// Called before any modules start. + /// + /// The pipeline hook context. + /// A task representing the asynchronous operation. + Task OnPipelineStartAsync(IPipelineContext context) => Task.CompletedTask; + + /// + /// Called after all modules finish. + /// + /// The pipeline hook context. + /// The summary of all registered module results. + /// A task representing the asynchronous operation. + Task OnPipelineEndAsync(IPipelineContext context, PipelineSummary pipelineSummary) => Task.CompletedTask; +} diff --git a/src/ModularPipelines/Events/IPlanningSafeModuleRegistrationHandler.cs b/src/ModularPipelines/Events/IPlanningSafeModuleRegistrationHandler.cs new file mode 100644 index 00000000000..be9cbbeca5b --- /dev/null +++ b/src/ModularPipelines/Events/IPlanningSafeModuleRegistrationHandler.cs @@ -0,0 +1,10 @@ +namespace ModularPipelines.Events; + +/// +/// Marks a module registration handler as safe to invoke while planning a dependency graph. +/// +/// +/// Implement this interface only when the handler is deterministic, idempotent, and free of +/// external side effects. +/// +public interface IPlanningSafeModuleRegistrationHandler : IModuleRegistrationHandler; diff --git a/src/ModularPipelines/Extensions/PipelineBuilderExtensions.cs b/src/ModularPipelines/Extensions/PipelineBuilderExtensions.cs index 0af0c002181..538db873689 100644 --- a/src/ModularPipelines/Extensions/PipelineBuilderExtensions.cs +++ b/src/ModularPipelines/Extensions/PipelineBuilderExtensions.cs @@ -5,6 +5,7 @@ using ModularPipelines.Caching; using ModularPipelines.Engine; using ModularPipelines.Enums; +using ModularPipelines.Events; using ModularPipelines.Extensions; using ModularPipelines.Interfaces; using ModularPipelines.Modules; @@ -151,32 +152,32 @@ public static PipelineBuilder AddRequirement( } /// - /// Adds global hooks to run before or after all the modules have executed. + /// Adds a handler for pipeline lifecycle events. /// /// The pipeline builder. - /// The type of hook class. + /// The handler type. /// The same builder instance for chaining. - public static PipelineBuilder AddPipelineGlobalHooks< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TGlobalSetup>( + public static PipelineBuilder AddPipelineEventHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>( this PipelineBuilder builder) - where TGlobalSetup : class, IPipelineGlobalHooks + where THandler : class, IPipelineEventHandler { - builder.Services.AddPipelineGlobalHooks(); + builder.Services.AddPipelineEventHandler(); return builder; } /// - /// Adds a global receiver for module lifecycle events. + /// Adds a global handler for module lifecycle events. /// /// The pipeline builder. - /// The receiver type. + /// The handler type. /// The same builder instance for chaining. - public static PipelineBuilder AddModuleEventReceiver< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TReceiver>( + public static PipelineBuilder AddModuleEventHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>( this PipelineBuilder builder) - where TReceiver : class, IModuleEventReceiver + where THandler : class, IModuleEventHandler { - builder.Services.AddModuleEventReceiver(); + builder.Services.AddModuleEventHandler(); return builder; } diff --git a/src/ModularPipelines/Extensions/ServiceCollectionExtensions.cs b/src/ModularPipelines/Extensions/ServiceCollectionExtensions.cs index e10ccfeaf4a..2d77b0e53e1 100644 --- a/src/ModularPipelines/Extensions/ServiceCollectionExtensions.cs +++ b/src/ModularPipelines/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using ModularPipelines.DependencyInjection; using ModularPipelines.Engine; +using ModularPipelines.Events; using ModularPipelines.Generated; using ModularPipelines.Interfaces; using ModularPipelines.Modules; @@ -343,29 +344,29 @@ internal static HashSet GetRegisteredModuleTypes(IServiceCollection servic } /// - /// Adds global hooks to run before or after all the modules have executed. + /// Adds a handler for pipeline lifecycle events. /// /// The pipeline's service collection. - /// The type of hook class. + /// The handler type. /// The pipeline's same service collection. - internal static IServiceCollection AddPipelineGlobalHooks<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TGlobalSetup>(this IServiceCollection services) - where TGlobalSetup : class, IPipelineGlobalHooks + internal static IServiceCollection AddPipelineEventHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(this IServiceCollection services) + where THandler : class, IPipelineEventHandler { - return services.AddSingleton(); + return services.AddSingleton(); } /// - /// Adds a global receiver for module lifecycle events. + /// Adds a global handler for module lifecycle events. /// /// The pipeline's service collection. - /// The receiver type. + /// The handler type. /// The pipeline's same service collection. - internal static IServiceCollection AddModuleEventReceiver< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TReceiver>( + internal static IServiceCollection AddModuleEventHandler< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>( this IServiceCollection services) - where TReceiver : class, IModuleEventReceiver + where THandler : class, IModuleEventHandler { - return services.AddSingleton(); + return services.AddSingleton(); } internal static IServiceCollection AddServiceCollection(this IServiceCollection serviceCollection) diff --git a/src/ModularPipelines/Interfaces/IModuleEventReceiver.cs b/src/ModularPipelines/Interfaces/IModuleEventReceiver.cs deleted file mode 100644 index a721d181433..00000000000 --- a/src/ModularPipelines/Interfaces/IModuleEventReceiver.cs +++ /dev/null @@ -1,56 +0,0 @@ -using ModularPipelines.Context; - -namespace ModularPipelines.Interfaces; - -/// -/// Receives lifecycle events for every module in a pipeline. -/// -/// -/// -/// Register implementations with AddModuleEventReceiver<TReceiver>(). -/// All registered receivers are invoked concurrently for each event. -/// -/// -/// Global ready and start receivers run before attribute event handlers and module virtual hooks. -/// For completion events, module virtual hooks run first, followed by attribute event handlers -/// where applicable and then global failure or skipped receivers. Successful end receivers run -/// before attribute end handlers. -/// -/// -public interface IModuleEventReceiver -{ - /// - /// Called when a module's dependencies are satisfied and it is ready to execute. - /// - /// The module hook context. - /// A task representing the asynchronous operation. - Task OnModuleReadyAsync(IModuleHookContext context) => Task.CompletedTask; - - /// - /// Called when a module starts executing. - /// - /// The module hook context. - /// A task representing the asynchronous operation. - Task OnModuleStartAsync(IModuleHookContext context) => Task.CompletedTask; - - /// - /// Called after a module completes successfully. - /// - /// The module hook context. - /// A task representing the asynchronous operation. - Task OnModuleEndAsync(IModuleHookContext context) => Task.CompletedTask; - - /// - /// Called when a module fails with an exception. - /// - /// The module hook context. - /// A task representing the asynchronous operation. - Task OnModuleFailureAsync(IModuleHookContext context) => Task.CompletedTask; - - /// - /// Called when a module is skipped. - /// - /// The module hook context. - /// A task representing the asynchronous operation. - Task OnModuleSkippedAsync(IModuleHookContext context) => Task.CompletedTask; -} diff --git a/src/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs b/src/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs deleted file mode 100644 index 68134908d42..00000000000 --- a/src/ModularPipelines/Interfaces/IPipelineGlobalHooks.cs +++ /dev/null @@ -1,25 +0,0 @@ -using ModularPipelines.Context; -using ModularPipelines.Models; - -namespace ModularPipelines.Interfaces; - -/// -/// Hooks for pipeline-level lifecycle events (before any modules start, after all complete). -/// -public interface IPipelineGlobalHooks -{ - /// - /// Called before any modules have started. - /// - /// A pipeline hook context object provided by the pipeline. - /// A representing the result of the asynchronous operation. - Task OnPipelineStartAsync(IPipelineContext context) => Task.CompletedTask; - - /// - /// Called after all modules have finished. - /// - /// A pipeline hook context object provided by the pipeline. - /// A summary of the pipeline results, containing all of the registered modules. - /// A representing the result of the asynchronous operation. - Task OnPipelineEndAsync(IPipelineContext context, PipelineSummary pipelineSummary) => Task.CompletedTask; -} diff --git a/src/ModularPipelines/Modules/Module.cs b/src/ModularPipelines/Modules/Module.cs index 120af832ec8..7837d9c15eb 100644 --- a/src/ModularPipelines/Modules/Module.cs +++ b/src/ModularPipelines/Modules/Module.cs @@ -4,6 +4,7 @@ using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Engine; +using ModularPipelines.Events; using ModularPipelines.Interfaces; using ModularPipelines.Models; @@ -185,7 +186,7 @@ protected virtual Module CreatePlanningCopy(IServiceProvider serviceProvider) /// Edge case: If throws an exception, /// is called, but is not called /// because the before hook did not complete. Attribute failure handlers and registered - /// implementations are still notified. + /// implementations are still notified. /// /// protected virtual Task OnBeforeExecuteAsync(IModuleContext context, CancellationToken cancellationToken) diff --git a/src/ModularPipelines/PublicAPI.Shipped.txt b/src/ModularPipelines/PublicAPI.Shipped.txt index ab5f116a55c..0ff86941913 100644 --- a/src/ModularPipelines/PublicAPI.Shipped.txt +++ b/src/ModularPipelines/PublicAPI.Shipped.txt @@ -106,44 +106,6 @@ ModularPipelines.Attributes.DependsOnModulesWithTagAttribute.Tag.get -> string! ModularPipelines.Attributes.EnumValueAttribute ModularPipelines.Attributes.EnumValueAttribute.EnumValueAttribute(string! value) -> void ModularPipelines.Attributes.EnumValueAttribute.Value.get -> string! -ModularPipelines.Attributes.Events.IEventHandlerPriority -ModularPipelines.Attributes.Events.IEventHandlerPriority.Priority.get -> int -ModularPipelines.Attributes.Events.IModuleEndHandler -ModularPipelines.Attributes.Events.IModuleEndHandler.ContinueOnError.get -> bool -ModularPipelines.Attributes.Events.IModuleEndHandler.OnModuleEndAsync(ModularPipelines.Context.IModuleHookContext! context, ModularPipelines.Models.IModuleResult! result) -> System.Threading.Tasks.Task! -ModularPipelines.Attributes.Events.IModuleFailureHandler -ModularPipelines.Attributes.Events.IModuleFailureHandler.ContinueOnError.get -> bool -ModularPipelines.Attributes.Events.IModuleFailureHandler.OnModuleFailureAsync(ModularPipelines.Context.IModuleHookContext! context, System.Exception! exception) -> System.Threading.Tasks.Task! -ModularPipelines.Attributes.Events.IModuleReadyHandler -ModularPipelines.Attributes.Events.IModuleReadyHandler.ContinueOnError.get -> bool -ModularPipelines.Attributes.Events.IModuleReadyHandler.OnModuleReadyAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Attributes.Events.IModuleRegistrationContext -ModularPipelines.Attributes.Events.IModuleRegistrationContext.AddDependency(System.Type! moduleType) -> void -ModularPipelines.Attributes.Events.IModuleRegistrationContext.AddDependency() -> void -ModularPipelines.Attributes.Events.IModuleRegistrationContext.AddDependencyOnAll(System.Func! predicate) -> void -ModularPipelines.Attributes.Events.IModuleRegistrationContext.AddDependencyOnAll() -> void -ModularPipelines.Attributes.Events.IModuleRegistrationContext.Configuration.get -> Microsoft.Extensions.Configuration.IConfiguration! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.Environment.get -> Microsoft.Extensions.Hosting.IHostEnvironment! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.GetMetadata(string! key) -> T? -ModularPipelines.Attributes.Events.IModuleRegistrationContext.GetModulesAssignableTo() -> System.Collections.Generic.IEnumerable! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.GetModulesWithAttribute() -> System.Collections.Generic.IEnumerable! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.IsModuleRegistered(System.Type! moduleType) -> bool -ModularPipelines.Attributes.Events.IModuleRegistrationContext.IsModuleRegistered() -> bool -ModularPipelines.Attributes.Events.IModuleRegistrationContext.ModuleAttributes.get -> System.Collections.Generic.IReadOnlyList! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.ModuleType.get -> System.Type! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.RegisteredModuleTypes.get -> System.Collections.Generic.IReadOnlyList! -ModularPipelines.Attributes.Events.IModuleRegistrationContext.RemoveDependency() -> void -ModularPipelines.Attributes.Events.IModuleRegistrationContext.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection? -ModularPipelines.Attributes.Events.IModuleRegistrationContext.SetMetadata(string! key, object! value) -> void -ModularPipelines.Attributes.Events.IModuleRegistrationEventReceiver -ModularPipelines.Attributes.Events.IModuleRegistrationEventReceiver.OnRegistrationAsync(ModularPipelines.Attributes.Events.IModuleRegistrationContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Attributes.Events.IModuleSkippedHandler -ModularPipelines.Attributes.Events.IModuleSkippedHandler.ContinueOnError.get -> bool -ModularPipelines.Attributes.Events.IModuleSkippedHandler.OnModuleSkippedAsync(ModularPipelines.Context.IModuleHookContext! context, ModularPipelines.Models.SkipDecision! reason) -> System.Threading.Tasks.Task! -ModularPipelines.Attributes.Events.IModuleStartHandler -ModularPipelines.Attributes.Events.IModuleStartHandler.ContinueOnError.get -> bool -ModularPipelines.Attributes.Events.IModuleStartHandler.OnModuleStartAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Attributes.Events.IPlanningSafeModuleRegistrationEventReceiver ModularPipelines.Attributes.ExecutionHintAttribute ModularPipelines.Attributes.IConditionAttribute ModularPipelines.Attributes.IConditionAttribute.ConditionNames.get -> string! @@ -855,17 +817,8 @@ ModularPipelines.Generated.IncompleteRuntimeMetadataAttribute.MetadataName.get - ModularPipelines.IConsoleWriter ModularPipelines.IConsoleWriter.LogToConsole(string! value) -> void ModularPipelines.IConsoleWriter.Write(Spectre.Console.Rendering.IRenderable! renderable) -> void -ModularPipelines.Interfaces.IModuleEventReceiver -ModularPipelines.Interfaces.IModuleEventReceiver.OnModuleEndAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Interfaces.IModuleEventReceiver.OnModuleFailureAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Interfaces.IModuleEventReceiver.OnModuleReadyAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Interfaces.IModuleEventReceiver.OnModuleSkippedAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! -ModularPipelines.Interfaces.IModuleEventReceiver.OnModuleStartAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! ModularPipelines.Interfaces.IParallelLimit ModularPipelines.Interfaces.IParallelLimit.Limit.get -> int -ModularPipelines.Interfaces.IPipelineGlobalHooks -ModularPipelines.Interfaces.IPipelineGlobalHooks.OnPipelineEndAsync(ModularPipelines.Context.IPipelineContext! context, ModularPipelines.Models.PipelineSummary! pipelineSummary) -> System.Threading.Tasks.Task! -ModularPipelines.Interfaces.IPipelineGlobalHooks.OnPipelineStartAsync(ModularPipelines.Context.IPipelineContext! context) -> System.Threading.Tasks.Task! ModularPipelines.IPipeline ModularPipelines.IPipeline.ExportDependencyGraphAsync(ModularPipelines.Enums.DependencyGraphFormat format, string! path, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! ModularPipelines.IPipeline.PlanAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -2072,11 +2025,9 @@ static ModularPipelines.PipelineBuilderExtensions.AddModule(this Modula static ModularPipelines.PipelineBuilderExtensions.AddModule(this ModularPipelines.PipelineBuilder! builder, TModule! module) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddModuleCache(this ModularPipelines.PipelineBuilder! builder, System.Action? configure = null) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddModuleEstimatedTimeProvider(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! -static ModularPipelines.PipelineBuilderExtensions.AddModuleEventReceiver(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddModules(this ModularPipelines.PipelineBuilder! builder, params System.Type![]! moduleTypes) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddModulesFromAssembly(this ModularPipelines.PipelineBuilder! builder, System.Reflection.Assembly! assembly) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddModulesFromAssemblyContainingType(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! -static ModularPipelines.PipelineBuilderExtensions.AddPipelineGlobalHooks(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddRequirement(this ModularPipelines.PipelineBuilder! builder, ModularPipelines.Requirements.IPipelineRequirement! requirement) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddRequirement(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddRequirement(this ModularPipelines.PipelineBuilder! builder, System.Func! factory) -> ModularPipelines.PipelineBuilder! diff --git a/src/ModularPipelines/PublicAPI.Unshipped.txt b/src/ModularPipelines/PublicAPI.Unshipped.txt index 94ea9509692..0d9bd671330 100644 --- a/src/ModularPipelines/PublicAPI.Unshipped.txt +++ b/src/ModularPipelines/PublicAPI.Unshipped.txt @@ -211,6 +211,44 @@ ModularPipelines.Enums.ModuleStatus.Skipped = 5 -> ModularPipelines.Enums.Module ModularPipelines.Enums.ModuleStatus.Succeeded = 2 -> ModularPipelines.Enums.ModuleStatus ModularPipelines.Enums.ModuleStatus.TimedOut = 6 -> ModularPipelines.Enums.ModuleStatus ModularPipelines.Enums.ModuleStatus.Unknown = 11 -> ModularPipelines.Enums.ModuleStatus +ModularPipelines.Events.IEventHandler +ModularPipelines.Events.IEventHandler.ContinueOnError.get -> bool +ModularPipelines.Events.IEventHandler.Priority.get -> int +ModularPipelines.Events.IModuleEndHandler +ModularPipelines.Events.IModuleEndHandler.OnModuleEndAsync(ModularPipelines.Context.IModuleHookContext! context, ModularPipelines.Models.IModuleResult! result) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IModuleEventHandler +ModularPipelines.Events.IModuleFailureHandler +ModularPipelines.Events.IModuleFailureHandler.OnModuleFailureAsync(ModularPipelines.Context.IModuleHookContext! context, System.Exception! exception) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IModuleReadyHandler +ModularPipelines.Events.IModuleReadyHandler.OnModuleReadyAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IModuleRegistrationContext +ModularPipelines.Events.IModuleRegistrationContext.AddDependency(System.Type! moduleType) -> void +ModularPipelines.Events.IModuleRegistrationContext.AddDependency() -> void +ModularPipelines.Events.IModuleRegistrationContext.AddDependencyOnAll(System.Func! predicate) -> void +ModularPipelines.Events.IModuleRegistrationContext.AddDependencyOnAll() -> void +ModularPipelines.Events.IModuleRegistrationContext.Configuration.get -> Microsoft.Extensions.Configuration.IConfiguration! +ModularPipelines.Events.IModuleRegistrationContext.Environment.get -> Microsoft.Extensions.Hosting.IHostEnvironment! +ModularPipelines.Events.IModuleRegistrationContext.GetMetadata(string! key) -> T? +ModularPipelines.Events.IModuleRegistrationContext.GetModulesAssignableTo() -> System.Collections.Generic.IEnumerable! +ModularPipelines.Events.IModuleRegistrationContext.GetModulesWithAttribute() -> System.Collections.Generic.IEnumerable! +ModularPipelines.Events.IModuleRegistrationContext.IsModuleRegistered(System.Type! moduleType) -> bool +ModularPipelines.Events.IModuleRegistrationContext.IsModuleRegistered() -> bool +ModularPipelines.Events.IModuleRegistrationContext.ModuleAttributes.get -> System.Collections.Generic.IReadOnlyList! +ModularPipelines.Events.IModuleRegistrationContext.ModuleType.get -> System.Type! +ModularPipelines.Events.IModuleRegistrationContext.RegisteredModuleTypes.get -> System.Collections.Generic.IReadOnlyList! +ModularPipelines.Events.IModuleRegistrationContext.RemoveDependency() -> void +ModularPipelines.Events.IModuleRegistrationContext.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection? +ModularPipelines.Events.IModuleRegistrationContext.SetMetadata(string! key, object! value) -> void +ModularPipelines.Events.IModuleRegistrationHandler +ModularPipelines.Events.IModuleRegistrationHandler.OnRegistrationAsync(ModularPipelines.Events.IModuleRegistrationContext! context) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IModuleSkippedHandler +ModularPipelines.Events.IModuleSkippedHandler.OnModuleSkippedAsync(ModularPipelines.Context.IModuleHookContext! context, ModularPipelines.Models.SkipDecision! reason) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IModuleStartHandler +ModularPipelines.Events.IModuleStartHandler.OnModuleStartAsync(ModularPipelines.Context.IModuleHookContext! context) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IPipelineEventHandler +ModularPipelines.Events.IPipelineEventHandler.OnPipelineEndAsync(ModularPipelines.Context.IPipelineContext! context, ModularPipelines.Models.PipelineSummary! pipelineSummary) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IPipelineEventHandler.OnPipelineStartAsync(ModularPipelines.Context.IPipelineContext! context) -> System.Threading.Tasks.Task! +ModularPipelines.Events.IPlanningSafeModuleRegistrationHandler ModularPipelines.Exceptions.DependencyFailedException.FailingModuleType.get -> System.Type! ModularPipelines.Exceptions.ModuleSelfDependencyException ModularPipelines.Exceptions.ModuleSelfDependencyException.ModuleSelfDependencyException(string? message) -> void @@ -612,6 +650,8 @@ static ModularPipelines.Options.PowerShellScriptOptions.operator !=(ModularPipel static ModularPipelines.Options.PowerShellScriptOptions.operator ==(ModularPipelines.Options.PowerShellScriptOptions? left, ModularPipelines.Options.PowerShellScriptOptions? right) -> bool static ModularPipelines.Pipeline.CreateBuilder(ModularPipelines.PipelineBuilderSettings! settings, string! sourceFilePath = "") -> ModularPipelines.PipelineBuilder! static ModularPipelines.Pipeline.CreateBuilder(string![]? args = null, string! sourceFilePath = "") -> ModularPipelines.PipelineBuilder! +static ModularPipelines.PipelineBuilderExtensions.AddModuleEventHandler(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! +static ModularPipelines.PipelineBuilderExtensions.AddPipelineEventHandler(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderExtensions.AddValidator(this ModularPipelines.PipelineBuilder! builder) -> ModularPipelines.PipelineBuilder! static ModularPipelines.PipelineBuilderSettings.operator !=(ModularPipelines.PipelineBuilderSettings? left, ModularPipelines.PipelineBuilderSettings? right) -> bool static ModularPipelines.PipelineBuilderSettings.operator ==(ModularPipelines.PipelineBuilderSettings? left, ModularPipelines.PipelineBuilderSettings? right) -> bool diff --git a/test/ModularPipelines.TrimAotSmoke/Program.cs b/test/ModularPipelines.TrimAotSmoke/Program.cs index 9fc5417f26f..84d8e0af281 100644 --- a/test/ModularPipelines.TrimAotSmoke/Program.cs +++ b/test/ModularPipelines.TrimAotSmoke/Program.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Options; using ModularPipelines; using ModularPipelines.Attributes; -using ModularPipelines.Attributes.Events; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Engine; +using ModularPipelines.Events; using ModularPipelines.Extensions; using ModularPipelines.Models; using ModularPipelines.Modules; diff --git a/test/ModularPipelines.UnitTests/Artifacts/ArtifactContractTests.cs b/test/ModularPipelines.UnitTests/Artifacts/ArtifactContractTests.cs index dc06a43353b..9242fe4ea03 100644 --- a/test/ModularPipelines.UnitTests/Artifacts/ArtifactContractTests.cs +++ b/test/ModularPipelines.UnitTests/Artifacts/ArtifactContractTests.cs @@ -10,6 +10,7 @@ using ModularPipelines.Distributed; using ModularPipelines.Distributed.Artifacts; using ModularPipelines.Engine; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Interfaces; using ModularPipelines.Models; @@ -398,27 +399,27 @@ await File.ReadAllTextAsync( cancellationToken); } - private sealed class EndHookArtifactReceiver : IModuleEventReceiver + private sealed class EndHookArtifactHandler : IModuleEventHandler { - public Task OnModuleEndAsync(IModuleHookContext context) => + public Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result) => context.ModuleType == typeof(AfterHookArtifactProducerModule) ? File.WriteAllTextAsync(AfterHookProducedFile, "end-hook") : Task.CompletedTask; } - private sealed class AwaitingEndHookReceiver : IModuleEventReceiver + private sealed class AwaitingEndHookHandler : IModuleEventHandler { public static Enums.ModuleStatus? ObservedStatus { get; set; } - public async Task OnModuleEndAsync(IModuleHookContext context) + public async Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result) { if (context.ModuleType != typeof(LocalProducerModule)) { return; } - var result = await ((IInternalModule) context.Module).ResultTask.WaitAsync(TimeSpan.FromSeconds(5)); - ObservedStatus = result.Status; + var awaitedResult = await ((IInternalModule) context.Module).ResultTask.WaitAsync(TimeSpan.FromSeconds(5)); + ObservedStatus = awaitedResult.Status; } } @@ -1537,7 +1538,7 @@ public async Task StandaloneExecutionRestoresConsumedArtifact() { DeleteLocalArtifacts(); LocalConsumerModule.ConsumedContent = null; - AwaitingEndHookReceiver.ObservedStatus = null; + AwaitingEndHookHandler.ObservedStatus = null; RecordingResultRepository.SaveCount = 0; try @@ -1719,14 +1720,14 @@ public async Task StandaloneExecutionUploadsArtifactsAfterModuleAfterHook() } [Test] - public async Task StandaloneExecutionUploadsArtifactsAfterModuleEndReceiver() + public async Task StandaloneExecutionUploadsArtifactsAfterModuleEndHandler() { DeleteLocalArtifacts(); try { var builder = Pipeline.CreateBuilder(); - builder.AddModuleEventReceiver(); + builder.AddModuleEventHandler(); builder.AddModule(); builder.AddModule(); @@ -1984,7 +1985,7 @@ public async Task StandaloneExecutionFailsProducerWhenRequiredArtifactUploadFail var builder = Pipeline.CreateBuilder(); builder.Services.AddSingleton(); builder.AddResultsRepository(); - builder.AddModuleEventReceiver(); + builder.AddModuleEventHandler(); builder.AddModule(); builder.AddModule(); await using var pipeline = await builder.BuildAsync(); @@ -2007,7 +2008,7 @@ public async Task StandaloneExecutionFailsProducerWhenRequiredArtifactUploadFail await Assert.That(producerResult!.Status).IsEqualTo(Enums.ModuleStatus.Failed); await Assert.That(awaitedProducerResult.Status).IsEqualTo(Enums.ModuleStatus.Failed); await Assert.That(RecordingResultRepository.SaveCount).IsEqualTo(0); - await Assert.That(AwaitingEndHookReceiver.ObservedStatus) + await Assert.That(AwaitingEndHookHandler.ObservedStatus) .IsEqualTo(Enums.ModuleStatus.Succeeded); await Assert.That(LocalConsumerModule.ConsumedContent).IsNull(); } diff --git a/test/ModularPipelines.UnitTests/Attributes/DynamicDependencyIntegrationTests.cs b/test/ModularPipelines.UnitTests/Attributes/DynamicDependencyIntegrationTests.cs index e7550601695..060a4cd4850 100644 --- a/test/ModularPipelines.UnitTests/Attributes/DynamicDependencyIntegrationTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/DynamicDependencyIntegrationTests.cs @@ -1,5 +1,5 @@ -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Modules; using ModularPipelines.TestHelpers; @@ -11,7 +11,7 @@ public class DynamicDependencyIntegrationTests : TestBase { private static readonly List ExecutionOrder = new(); - public class AddDependencyAttribute : Attribute, IModuleRegistrationEventReceiver + public class AddDependencyAttribute : Attribute, IModuleRegistrationHandler { private readonly Type _dependencyType; diff --git a/test/ModularPipelines.UnitTests/Attributes/AttributeEventInvokerTests.cs b/test/ModularPipelines.UnitTests/Attributes/EventHandlerInvokerTests.cs similarity index 55% rename from test/ModularPipelines.UnitTests/Attributes/AttributeEventInvokerTests.cs rename to test/ModularPipelines.UnitTests/Attributes/EventHandlerInvokerTests.cs index 130bfcb81e6..dcb1aa4c9e8 100644 --- a/test/ModularPipelines.UnitTests/Attributes/AttributeEventInvokerTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/EventHandlerInvokerTests.cs @@ -1,12 +1,12 @@ using Microsoft.Extensions.Logging; -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; using ModularPipelines.Engine.Attributes; +using ModularPipelines.Events; using Moq; namespace ModularPipelines.UnitTests.Attributes; -public class AttributeEventInvokerTests +public class EventHandlerInvokerTests { private class SuccessfulHandler : IModuleStartHandler { @@ -47,7 +47,7 @@ public async Task InvokeAsync_CallsAllHandlers() var handler1 = new SuccessfulHandler(); var handler2 = new SuccessfulHandler(); var handlers = new List { handler1, handler2 }; - var invoker = new AttributeEventInvoker(Mock.Of>()); + var invoker = new EventHandlerInvoker(Mock.Of>()); var context = Mock.Of(); await invoker.InvokeStartHandlersAsync(handlers, context); @@ -61,12 +61,47 @@ public async Task InvokeAsync_HandlerThrows_ContinueOnErrorFalse_Propagates() { var handler = new FailingHandler(); var handlers = new List { handler }; - var invoker = new AttributeEventInvoker(Mock.Of>()); + var logger = new Mock>(); + var invoker = new EventHandlerInvoker(logger.Object); var context = Mock.Of(); await Assert.That(async () => await invoker.InvokeStartHandlersAsync(handlers, context)) .ThrowsException() .WithMessage("Test exception"); + logger.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((state, _) => + state.ToString()!.Contains("Start handler FailingHandler failed", StringComparison.Ordinal)), + It.IsAny(), + It.IsAny>()), Times.Once); + } + + [Test] + public async Task InvokeAsync_HandlerThrows_ContinueOnErrorFalse_StillCallsRemainingHandlers() + { + var successHandler = new SuccessfulHandler(); + var handlers = new List { new FailingHandler(), successHandler }; + var invoker = new EventHandlerInvoker(Mock.Of>()); + var context = Mock.Of(); + + await Assert.That(async () => await invoker.InvokeStartHandlersAsync(handlers, context)) + .ThrowsException() + .WithMessage("Test exception"); + await Assert.That(successHandler.WasCalled).IsTrue(); + } + + [Test] + public async Task InvokeAsync_MultipleHandlersThrow_AggregatesFailures() + { + var handlers = new List { new FailingHandler(), new FailingHandler() }; + var invoker = new EventHandlerInvoker(Mock.Of>()); + var context = Mock.Of(); + + var exception = await Assert.That(async () => await invoker.InvokeStartHandlersAsync(handlers, context)) + .Throws(); + + await Assert.That(exception!.InnerExceptions).Count().IsEqualTo(2); } [Test] @@ -75,7 +110,7 @@ public async Task InvokeAsync_HandlerThrows_ContinueOnErrorTrue_Continues() var failingHandler = new FailingHandlerWithContinue(); var successHandler = new SuccessfulHandler(); var handlers = new List { failingHandler, successHandler }; - var invoker = new AttributeEventInvoker(Mock.Of>()); + var invoker = new EventHandlerInvoker(Mock.Of>()); var context = Mock.Of(); await invoker.InvokeStartHandlersAsync(handlers, context); diff --git a/test/ModularPipelines.UnitTests/Attributes/GeneratedAttributeEventMetadataTests.cs b/test/ModularPipelines.UnitTests/Attributes/GeneratedAttributeEventMetadataTests.cs index 8a8280923cd..74bc65f5ce8 100644 --- a/test/ModularPipelines.UnitTests/Attributes/GeneratedAttributeEventMetadataTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/GeneratedAttributeEventMetadataTests.cs @@ -1,7 +1,7 @@ using ModularPipelines.Attributes; -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; using ModularPipelines.Engine.Attributes; +using ModularPipelines.Events; using ModularPipelines.Modules; using ModularPipelines.Generated; @@ -9,7 +9,7 @@ namespace ModularPipelines.UnitTests.Attributes; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] -internal sealed class GeneratedStartAttribute(string name) : Attribute, IModuleStartHandler, IEventHandlerPriority +internal sealed class GeneratedStartAttribute(string name) : Attribute, IModuleStartHandler { public string Name { get; } = name; diff --git a/test/ModularPipelines.UnitTests/Attributes/LifecycleEventIntegrationTests.cs b/test/ModularPipelines.UnitTests/Attributes/LifecycleEventIntegrationTests.cs index 310ab321af3..c23f7e21ef6 100644 --- a/test/ModularPipelines.UnitTests/Attributes/LifecycleEventIntegrationTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/LifecycleEventIntegrationTests.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using ModularPipelines.Attributes.Events; using ModularPipelines.Configuration; using ModularPipelines.Conditions; using ModularPipelines.Context; +using ModularPipelines.Events; using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.Options; @@ -40,6 +40,7 @@ public class LogFailedAttribute : Attribute, IModuleFailureHandler public Task OnModuleFailureAsync(IModuleHookContext context, Exception exception) { EventLog.Add($"Failed:{context.ModuleName}:{exception.Message}"); + EventLog.Add($"FailedResult:{context.Result?.Status}"); return Task.CompletedTask; } } @@ -143,6 +144,7 @@ await TestPipelineBuilder.Create() await Assert.That(EventLog).Contains("Start:FailingModule"); await Assert.That(EventLog.Any(e => e.StartsWith("Failed:FailingModule:"))).IsTrue(); + await Assert.That(EventLog).Contains("FailedResult:Failed"); } [Test] diff --git a/test/ModularPipelines.UnitTests/Attributes/MetadataCrossPhaseIntegrationTests.cs b/test/ModularPipelines.UnitTests/Attributes/MetadataCrossPhaseIntegrationTests.cs index d1a3294070d..fcc0342d86e 100644 --- a/test/ModularPipelines.UnitTests/Attributes/MetadataCrossPhaseIntegrationTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/MetadataCrossPhaseIntegrationTests.cs @@ -1,5 +1,5 @@ -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; +using ModularPipelines.Events; using ModularPipelines.Models; using ModularPipelines.Modules; using ModularPipelines.TestHelpers; @@ -11,7 +11,7 @@ public class MetadataCrossPhaseIntegrationTests : TestBase { private static readonly List EventLog = new(); - public class SetMetadataOnRegistrationAttribute : Attribute, IModuleRegistrationEventReceiver + public class SetMetadataOnRegistrationAttribute : Attribute, IModuleRegistrationHandler { private readonly string _key; private readonly string _value; diff --git a/test/ModularPipelines.UnitTests/Attributes/ModuleAttributeEventServiceTests.cs b/test/ModularPipelines.UnitTests/Attributes/ModuleAttributeEventServiceTests.cs index aa8900e8c47..ed231377a71 100644 --- a/test/ModularPipelines.UnitTests/Attributes/ModuleAttributeEventServiceTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/ModuleAttributeEventServiceTests.cs @@ -1,6 +1,6 @@ -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; using ModularPipelines.Engine.Attributes; +using ModularPipelines.Events; using ModularPipelines.Modules; namespace ModularPipelines.UnitTests.Attributes; @@ -39,7 +39,7 @@ public CountingAttribute() /// A start handler with priority 100 (runs last). /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class LowPriorityStartAttribute : Attribute, IModuleStartHandler, IEventHandlerPriority + public class LowPriorityStartAttribute : Attribute, IModuleStartHandler { public bool ContinueOnError => false; public int Priority => 100; @@ -51,7 +51,7 @@ public class LowPriorityStartAttribute : Attribute, IModuleStartHandler, IEventH /// A start handler with priority 10 (runs second). /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class MediumPriorityStartAttribute : Attribute, IModuleStartHandler, IEventHandlerPriority + public class MediumPriorityStartAttribute : Attribute, IModuleStartHandler { public bool ContinueOnError => false; public int Priority => 10; @@ -63,7 +63,7 @@ public class MediumPriorityStartAttribute : Attribute, IModuleStartHandler, IEve /// A start handler with priority 1 (runs first with explicit priority). /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class HighPriorityStartAttribute : Attribute, IModuleStartHandler, IEventHandlerPriority + public class HighPriorityStartAttribute : Attribute, IModuleStartHandler { public bool ContinueOnError => false; public int Priority => 1; diff --git a/test/ModularPipelines.UnitTests/Attributes/ModuleReadyEventTests.cs b/test/ModularPipelines.UnitTests/Attributes/ModuleReadyEventTests.cs index 65295d9bccc..d48a1874e63 100644 --- a/test/ModularPipelines.UnitTests/Attributes/ModuleReadyEventTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/ModuleReadyEventTests.cs @@ -1,7 +1,7 @@ using ModularPipelines.Attributes; -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; using ModularPipelines.Enums; +using ModularPipelines.Events; using ModularPipelines.Modules; using ModularPipelines.TestHelpers; diff --git a/test/ModularPipelines.UnitTests/Attributes/ModuleRegistrationContextTests.cs b/test/ModularPipelines.UnitTests/Attributes/ModuleRegistrationContextTests.cs index 11886ba2296..7c110efe7dd 100644 --- a/test/ModularPipelines.UnitTests/Attributes/ModuleRegistrationContextTests.cs +++ b/test/ModularPipelines.UnitTests/Attributes/ModuleRegistrationContextTests.cs @@ -3,10 +3,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using ModularPipelines.Attributes.Events; using ModularPipelines.Context; using ModularPipelines.Engine.Attributes; using ModularPipelines.Engine.Dependencies; +using ModularPipelines.Events; using ModularPipelines.Modules; using ModularPipelines.Options; using Moq; diff --git a/test/ModularPipelines.UnitTests/CommandLine/PipelineCommandLineTests.cs b/test/ModularPipelines.UnitTests/CommandLine/PipelineCommandLineTests.cs index 1c74493d375..9315491cf90 100644 --- a/test/ModularPipelines.UnitTests/CommandLine/PipelineCommandLineTests.cs +++ b/test/ModularPipelines.UnitTests/CommandLine/PipelineCommandLineTests.cs @@ -1,12 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using ModularPipelines.Attributes; -using ModularPipelines.Attributes.Events; using ModularPipelines.Caching; using ModularPipelines.Conditions; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Engine; using ModularPipelines.Enums; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Extensions; using ModularPipelines.Interfaces; @@ -47,7 +47,7 @@ public void Write(IRenderable renderable) [AttributeUsage(AttributeTargets.Class)] private sealed class AddRegistrationDependencyAttribute(Type dependencyType) - : Attribute, IModuleRegistrationEventReceiver + : Attribute, IPlanningSafeModuleRegistrationHandler { public Task OnRegistrationAsync(IModuleRegistrationContext context) { diff --git a/test/ModularPipelines.UnitTests/Context/InterfaceVisibilityTests.cs b/test/ModularPipelines.UnitTests/Context/InterfaceVisibilityTests.cs index 384dbffaeeb..0b0fdf6afcf 100644 --- a/test/ModularPipelines.UnitTests/Context/InterfaceVisibilityTests.cs +++ b/test/ModularPipelines.UnitTests/Context/InterfaceVisibilityTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using ModularPipelines.Context; +using ModularPipelines.Events; namespace ModularPipelines.UnitTests.Context; @@ -147,8 +148,17 @@ public async Task ExtensionPointInterfaces_ShouldBePublic() var extensionPointInterfaces = new[] { ("ModularPipelines", "IPipeline"), - ("ModularPipelines.Interfaces", "IPipelineGlobalHooks"), - ("ModularPipelines.Interfaces", "IModuleEventReceiver"), + ("ModularPipelines.Events", "IEventHandler"), + ("ModularPipelines.Events", "IPipelineEventHandler"), + ("ModularPipelines.Events", "IModuleEventHandler"), + ("ModularPipelines.Events", "IModuleReadyHandler"), + ("ModularPipelines.Events", "IModuleStartHandler"), + ("ModularPipelines.Events", "IModuleEndHandler"), + ("ModularPipelines.Events", "IModuleFailureHandler"), + ("ModularPipelines.Events", "IModuleSkippedHandler"), + ("ModularPipelines.Events", "IModuleRegistrationHandler"), + ("ModularPipelines.Events", "IPlanningSafeModuleRegistrationHandler"), + ("ModularPipelines.Events", "IModuleRegistrationContext"), ("ModularPipelines.Requirements", "IPipelineRequirement") }; @@ -161,6 +171,40 @@ await Assert.That(iface).IsNotNull() await Assert.That(iface!.IsPublic).IsTrue() .Because($"{name} should be public"); } + + await Assert.That(assembly.GetType("ModularPipelines.Interfaces.IPipelineGlobalHooks")).IsNull(); + await Assert.That(assembly.GetType("ModularPipelines.Interfaces.IModuleEventReceiver")).IsNull(); + await Assert.That(assembly.GetType("ModularPipelines.Attributes.Events.IModuleStartHandler")).IsNull(); + await Assert.That(assembly.GetType("ModularPipelines.Attributes.Events.IModuleRegistrationEventReceiver")).IsNull(); + } + + [Test] + public async Task ModuleEventHandler_Composes_One_Shared_Handler_Family() + { + var modulePhaseHandlers = new[] + { + typeof(IModuleReadyHandler), + typeof(IModuleStartHandler), + typeof(IModuleEndHandler), + typeof(IModuleFailureHandler), + typeof(IModuleSkippedHandler), + typeof(IModuleRegistrationHandler), + }; + + foreach (var handlerType in modulePhaseHandlers.Append(typeof(IPipelineEventHandler))) + { + await Assert.That(typeof(IEventHandler).IsAssignableFrom(handlerType)).IsTrue(); + } + + foreach (var handlerType in modulePhaseHandlers.Take(5)) + { + await Assert.That(handlerType.IsAssignableFrom(typeof(IModuleEventHandler))).IsTrue(); + } + + await Assert.That(typeof(IEventHandler).GetProperty(nameof(IEventHandler.ContinueOnError))).IsNotNull(); + await Assert.That(typeof(IEventHandler).GetProperty(nameof(IEventHandler.Priority))).IsNotNull(); + await Assert.That(typeof(IModuleRegistrationHandler) + .IsAssignableFrom(typeof(IPlanningSafeModuleRegistrationHandler))).IsTrue(); } [Test] diff --git a/test/ModularPipelines.UnitTests/Engine/DependencyGraphExporterTests.cs b/test/ModularPipelines.UnitTests/Engine/DependencyGraphExporterTests.cs index 891e1ed884f..6e46e32125c 100644 --- a/test/ModularPipelines.UnitTests/Engine/DependencyGraphExporterTests.cs +++ b/test/ModularPipelines.UnitTests/Engine/DependencyGraphExporterTests.cs @@ -3,13 +3,13 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using ModularPipelines.Attributes; -using ModularPipelines.Attributes.Events; using ModularPipelines.Conditions; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Engine; using ModularPipelines.Engine.Attributes; using ModularPipelines.Engine.Dependencies; +using ModularPipelines.Events; using ModularPipelines.Enums; using ModularPipelines.Exceptions; using ModularPipelines.Extensions; @@ -33,6 +33,7 @@ public class DependencyGraphExporterTests private static int _planningActivations; private static int _planningDisposals; private static int _planningRegistrationEvents; + private static int _unsafeRegistrationConstructions; private static int _directModuleActivations; private static CancellationTokenSource? _planningCancellation; private static readonly object DependencySentinel = new(); @@ -95,7 +96,7 @@ private sealed class DependencyModule : Module [AttributeUsage(AttributeTargets.Class)] private sealed class AddRegistrationDependencyAttribute(Type dependencyType) - : Attribute, IPlanningSafeModuleRegistrationEventReceiver + : Attribute, IPlanningSafeModuleRegistrationHandler { public Task OnRegistrationAsync(IModuleRegistrationContext context) { @@ -115,7 +116,7 @@ public PlanningCompanionAttribute() [AttributeUsage(AttributeTargets.Class)] private sealed class AddDependencyWhenCompanionPresentAttribute(Type dependencyType) - : Attribute, IPlanningSafeModuleRegistrationEventReceiver + : Attribute, IPlanningSafeModuleRegistrationHandler { public Task OnRegistrationAsync(IModuleRegistrationContext context) { @@ -139,7 +140,7 @@ public PlanningPresenceAttribute() [AttributeUsage(AttributeTargets.Class)] private sealed class AddStartupDependencyAttribute(Type dependencyType) - : Attribute, IModuleRegistrationEventReceiver + : Attribute, IModuleRegistrationHandler { public Task OnRegistrationAsync(IModuleRegistrationContext context) { @@ -474,7 +475,7 @@ public Task EvaluateAsync(IPipelineContext context) => Task.FromResult(_startupConditionEnabled); } - private sealed class EnableStartupConditionHook : IPipelineGlobalHooks + private sealed class EnableStartupConditionHook : IPipelineEventHandler { public Task OnPipelineStartAsync(IPipelineContext context) { @@ -483,7 +484,7 @@ public Task OnPipelineStartAsync(IPipelineContext context) } } - private sealed class EnableStartupDependencyHook : IPipelineGlobalHooks + private sealed class EnableStartupDependencyHook : IPipelineEventHandler { public Task OnPipelineStartAsync(IPipelineContext context) { @@ -492,7 +493,7 @@ public Task OnPipelineStartAsync(IPipelineContext context) } } - private sealed class EnableStartupConfigurationHook : IPipelineGlobalHooks + private sealed class EnableStartupConfigurationHook : IPipelineEventHandler { public Task OnPipelineStartAsync(IPipelineContext context) { @@ -1361,8 +1362,7 @@ public void Dispose() } private sealed class CountPlanningRegistrationAttribute - : Attribute, IModuleRegistrationEventReceiver, - IPlanningSafeModuleRegistrationEventReceiver + : Attribute, IPlanningSafeModuleRegistrationHandler { public Task OnRegistrationAsync(IModuleRegistrationContext context) { @@ -1440,8 +1440,13 @@ private sealed class UnsafeRegistrationModule : Module } private sealed class CountUnsafeRegistrationAttribute - : Attribute, IModuleRegistrationEventReceiver + : Attribute, IModuleRegistrationHandler { + public CountUnsafeRegistrationAttribute() + { + Interlocked.Increment(ref _unsafeRegistrationConstructions); + } + public Task OnRegistrationAsync(IModuleRegistrationContext context) { Interlocked.Increment(ref _planningRegistrationEvents); @@ -1459,8 +1464,7 @@ private sealed class CancelPlanningModule : Module } private sealed class CancelPlanningRegistrationAttribute - : Attribute, IModuleRegistrationEventReceiver, - IPlanningSafeModuleRegistrationEventReceiver + : Attribute, IPlanningSafeModuleRegistrationHandler { public Task OnRegistrationAsync(IModuleRegistrationContext context) { @@ -3193,7 +3197,7 @@ public async Task Render_Does_Not_Cache_Conditions_Before_Startup_Hooks() { var builder = Pipeline.CreateBuilder(); builder.AddModule(); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await using var pipeline = await builder.BuildAsync(); var exporter = pipeline.Services.GetRequiredService(); @@ -3217,7 +3221,7 @@ public async Task Rejected_Render_Does_Not_Cache_Registration_Before_Startup_Hoo var builder = Pipeline.CreateBuilder(); builder.AddModule(); builder.AddModule(); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await using var pipeline = await builder.BuildAsync(); var exporter = pipeline.Services.GetRequiredService(); var dependencyRegistry = pipeline.Services.GetRequiredService(); @@ -3245,7 +3249,7 @@ public async Task Rejected_Render_Does_Not_Freeze_Direct_Module_Configuration() { var builder = Pipeline.CreateBuilder(); builder.AddModule(new StartupConfiguredModule()); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await using var pipeline = await builder.BuildAsync(); var exporter = pipeline.Services.GetRequiredService(); @@ -3266,7 +3270,7 @@ public async Task Rejected_Render_Does_Not_Freeze_Factory_Module_Configuration() { var builder = Pipeline.CreateBuilder(); builder.AddModule(_ => new StartupConfiguredModule()); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await using var pipeline = await builder.BuildAsync(); var exporter = pipeline.Services.GetRequiredService(); @@ -3342,24 +3346,27 @@ public async Task Render_Preserves_User_Factory_Initialization() } [Test] - public async Task Rejected_Render_Then_Run_Invokes_Registration_Receivers_Once() + public async Task Rejected_Render_Does_Not_Construct_Unsafe_Handler_During_Planning() { var builder = Pipeline.CreateBuilder(); builder.AddModule(); await using var pipeline = await builder.BuildAsync(); var exporter = pipeline.Services.GetRequiredService(); _planningRegistrationEvents = 0; + _unsafeRegistrationConstructions = 0; var exception = await Assert.ThrowsAsync( () => exporter.RenderAsync(DependencyGraphFormat.Json)); var eventsAfterRender = _planningRegistrationEvents; + var constructionsAfterRender = _unsafeRegistrationConstructions; _ = await pipeline.RunAsync(); using (Assert.Multiple()) { await Assert.That(exception!.Message) - .Contains(nameof(IPlanningSafeModuleRegistrationEventReceiver)); + .Contains(nameof(IPlanningSafeModuleRegistrationHandler)); await Assert.That(eventsAfterRender).IsEqualTo(0); + await Assert.That(constructionsAfterRender).IsEqualTo(0); await Assert.That(_planningRegistrationEvents).IsEqualTo(1); } } diff --git a/test/ModularPipelines.UnitTests/Engine/Execution/ParallelLimitHandlerTests.cs b/test/ModularPipelines.UnitTests/Engine/Execution/ParallelLimitHandlerTests.cs index 9273c278354..d52ce99450b 100644 --- a/test/ModularPipelines.UnitTests/Engine/Execution/ParallelLimitHandlerTests.cs +++ b/test/ModularPipelines.UnitTests/Engine/Execution/ParallelLimitHandlerTests.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using ModularPipelines.Attributes.Events; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Engine; @@ -82,15 +81,15 @@ public async Task ModuleRunner_FiresGlobalAndAttributeReadyOnceBeforeLimitsAndMa parallelLimitHandler .Setup(x => x.AcquireExecutionHintLimitAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Mock.Of()); - var receiver = new Mock(); - receiver + var handler = new Mock(); + handler .Setup(x => x.OnModuleReadyAsync(It.IsAny())) .Returns(Task.CompletedTask); var builder = TestPipelineBuilder.Create() .AddModule(); builder.Services.AddSingleton(parallelLimitHandler.Object); - builder.Services.AddSingleton(receiver.Object); + builder.Services.AddSingleton(handler.Object); await using var host = await builder.BuildAsync(); var moduleRunner = host.Services.GetRequiredService(); var scheduler = new Mock(); @@ -105,7 +104,7 @@ public async Task ModuleRunner_FiresGlobalAndAttributeReadyOnceBeforeLimitsAndMa CancellationToken.None); await limitWaitObserved.Task.WaitAsync(TimeSpan.FromSeconds(2)); - receiver.Verify(x => x.OnModuleReadyAsync(It.IsAny()), Times.Once); + handler.Verify(x => x.OnModuleReadyAsync(It.IsAny()), Times.Once); await Assert.That(TrackingReadyAttribute.InvocationCount).IsEqualTo(1); scheduler.Verify(x => x.MarkModuleStarted(typeof(ReadyTestModule)), Times.Never); @@ -116,7 +115,7 @@ public async Task ModuleRunner_FiresGlobalAndAttributeReadyOnceBeforeLimitsAndMa await moduleRunner.ExecuteAsync(moduleState, scheduler.Object, CancellationToken.None); - receiver.Verify(x => x.OnModuleReadyAsync(It.IsAny()), Times.Once); + handler.Verify(x => x.OnModuleReadyAsync(It.IsAny()), Times.Once); await Assert.That(TrackingReadyAttribute.InvocationCount).IsEqualTo(1); scheduler.Verify(x => x.MarkModuleStarted(typeof(ReadyTestModule)), Times.Exactly(2)); } @@ -358,7 +357,7 @@ await Assert.That(exception!.CancellationToken) public async Task ModuleRunner_RoutesThrowingReadyHandlerThroughFailureLifecycle() { ThrowingReadyAttribute.Reset(); - var receiver = new TrackingFailureReceiver(); + var handler = new TrackingFailureHandler(); var mediator = new Mock(); mediator .Setup(x => x.Publish( @@ -367,7 +366,7 @@ public async Task ModuleRunner_RoutesThrowingReadyHandlerThroughFailureLifecycle .Returns(ValueTask.CompletedTask); var builder = TestPipelineBuilder.Create() .AddModule(); - builder.Services.AddSingleton(receiver); + builder.Services.AddSingleton(handler); builder.Services.AddSingleton(mediator.Object); await using var host = await builder.BuildAsync(); var moduleRunner = host.Services.GetRequiredService(); @@ -385,7 +384,7 @@ await Assert.ThrowsAsync(() => await Assert.That(resultRegistry.GetResult(typeof(ThrowingReadyTestModule))!.Status) .IsEqualTo(ModuleStatus.Failed); await Assert.That(ThrowingReadyAttribute.FailureInvocationCount).IsEqualTo(1); - await Assert.That(receiver.FailureInvocationCount).IsEqualTo(1); + await Assert.That(handler.FailureInvocationCount).IsEqualTo(1); } mediator.Verify(x => x.Publish( @@ -499,13 +498,13 @@ public Task OnModuleFailureAsync(IModuleHookContext context, Exception exception } } - private sealed class TrackingFailureReceiver : IModuleEventReceiver + private sealed class TrackingFailureHandler : IModuleEventHandler { private int _failureInvocationCount; public int FailureInvocationCount => Volatile.Read(ref _failureInvocationCount); - public Task OnModuleFailureAsync(IModuleHookContext context) + public Task OnModuleFailureAsync(IModuleHookContext context, Exception exception) { Interlocked.Increment(ref _failureInvocationCount); return Task.CompletedTask; diff --git a/test/ModularPipelines.UnitTests/Engine/FailedModuleNotificationTests.cs b/test/ModularPipelines.UnitTests/Engine/FailedModuleNotificationTests.cs index d6ec46e789c..bb572fef570 100644 --- a/test/ModularPipelines.UnitTests/Engine/FailedModuleNotificationTests.cs +++ b/test/ModularPipelines.UnitTests/Engine/FailedModuleNotificationTests.cs @@ -37,7 +37,7 @@ await TestPipelineBuilder.Create() } [Test] - public async Task Failed_Module_Publishes_Completion_When_Failure_Receiver_Throws() + public async Task Failed_Module_Publishes_Completion_When_Failure_Handler_Throws() { var mediator = new Mock(); mediator @@ -51,7 +51,7 @@ await TestPipelineBuilder.Create() .ConfigureServices(services => { services.AddSingleton(mediator.Object); - services.AddSingleton(); + services.AddSingleton(); }) .AddModule() .RunAsync()) @@ -74,11 +74,11 @@ protected internal override Task ExecuteAsync( } } - private sealed class ThrowingFailureReceiver : IModuleEventReceiver + private sealed class ThrowingFailureHandler : IModuleEventHandler { - public Task OnModuleFailureAsync(IModuleHookContext context) + public Task OnModuleFailureAsync(IModuleHookContext context, Exception exception) { - return Task.FromException(new InvalidOperationException("Expected receiver failure")); + return Task.FromException(new InvalidOperationException("Expected handler failure")); } } } diff --git a/test/ModularPipelines.UnitTests/Engine/PipelineSetupExecutorTests.cs b/test/ModularPipelines.UnitTests/Engine/PipelineSetupExecutorTests.cs index bc110621ac6..30627fd7d39 100644 --- a/test/ModularPipelines.UnitTests/Engine/PipelineSetupExecutorTests.cs +++ b/test/ModularPipelines.UnitTests/Engine/PipelineSetupExecutorTests.cs @@ -1,8 +1,11 @@ +using Microsoft.Extensions.Logging; using ModularPipelines.Context; using ModularPipelines.Engine; using ModularPipelines.Engine.Attributes; using ModularPipelines.Engine.Dependencies; +using ModularPipelines.Events; using ModularPipelines.Interfaces; +using ModularPipelines.Models; using ModularPipelines.Modules; using Moq; @@ -11,6 +14,30 @@ namespace ModularPipelines.UnitTests.Engine; [TUnit.Core.NotInParallel(nameof(PipelineSetupExecutorTests))] public class PipelineSetupExecutorTests { + private sealed class OrderedPipelineHandler(int priority, string name, ICollection calls) + : IPipelineEventHandler + { + public int Priority => priority; + + public Task OnPipelineStartAsync(IPipelineContext context) + { + calls.Add(name); + return Task.CompletedTask; + } + } + + private sealed class OrderedModuleHandler(int priority, string name, ICollection calls) + : IModuleEventHandler + { + public int Priority => priority; + + public Task OnModuleReadyAsync(IModuleHookContext context) + { + calls.Add(name); + return Task.CompletedTask; + } + } + [AttributeUsage(AttributeTargets.Class)] private sealed class CountingAttribute : Attribute { @@ -36,6 +63,7 @@ public async Task OnModuleReadyAsync_WithoutHooks_DoesNotCreateAttributes() var executor = new PipelineSetupExecutor( [], [], + new EventHandlerInvoker(Mock.Of>()), Mock.Of(), Mock.Of(), new ModuleAttributeEventService()); @@ -51,14 +79,15 @@ public async Task OnModuleReadyAsync_WithoutHooks_DoesNotCreateAttributes() public async Task OnModuleReadyAsync_WithHook_UsesCachedAttributeInstances() { var attributeEventService = new ModuleAttributeEventService(); - IReadOnlyList? receiverAttributes = null; - var receiver = new Mock(); - receiver.Setup(x => x.OnModuleReadyAsync(It.IsAny())) - .Callback(context => receiverAttributes = context.ModuleAttributes) + IReadOnlyList? handlerAttributes = null; + var handler = new Mock(); + handler.Setup(x => x.OnModuleReadyAsync(It.IsAny())) + .Callback(context => handlerAttributes = context.ModuleAttributes) .Returns(Task.CompletedTask); var executor = new PipelineSetupExecutor( [], - [receiver.Object], + [handler.Object], + new EventHandlerInvoker(Mock.Of>()), Mock.Of(), Mock.Of(), attributeEventService); @@ -67,8 +96,80 @@ public async Task OnModuleReadyAsync_WithHook_UsesCachedAttributeInstances() await executor.OnModuleReadyAsync(new ModuleState(module, module.GetType())); await Assert.That(ReferenceEquals( - receiverAttributes, + handlerAttributes, attributeEventService.GetAttributes(module.GetType()))) .IsTrue(); } + + [Test] + public async Task EventHandlers_Run_In_Priority_Order() + { + var calls = new List(); + var pipelineContext = Mock.Of(); + var contextProvider = new Mock(); + contextProvider.Setup(x => x.GetModuleContext()).Returns(pipelineContext); + var executor = new PipelineSetupExecutor( + [ + new OrderedPipelineHandler(20, "pipeline-20", calls), + new OrderedPipelineHandler(10, "pipeline-10", calls), + ], + [ + new OrderedModuleHandler(20, "module-20", calls), + new OrderedModuleHandler(10, "module-10", calls), + ], + new EventHandlerInvoker(Mock.Of>()), + contextProvider.Object, + Mock.Of(), + new ModuleAttributeEventService()); + var module = new TestModule(); + + await executor.OnPipelineStartAsync(); + await executor.OnModuleReadyAsync(new ModuleState(module, module.GetType())); + + var expected = new[] + { + "pipeline-10", + "pipeline-20", + "module-10", + "module-20", + }; + + await Assert.That(calls).Count().IsEqualTo(expected.Length); + for (var index = 0; index < expected.Length; index++) + { + await Assert.That(calls[index]).IsEqualTo(expected[index]); + } + } + + [Test] + public async Task Completion_Arguments_Are_Forwarded_To_Module_Event_Handlers() + { + var handler = new Mock(); + handler.Setup(x => x.OnModuleEndAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + handler.Setup(x => x.OnModuleFailureAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + handler.Setup(x => x.OnModuleSkippedAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + var executor = new PipelineSetupExecutor( + [], + [handler.Object], + new EventHandlerInvoker(Mock.Of>()), + Mock.Of(), + Mock.Of(), + new ModuleAttributeEventService()); + var module = new TestModule(); + var moduleState = new ModuleState(module, module.GetType()); + var result = Mock.Of(); + var exception = new InvalidOperationException("Expected failure"); + var skipDecision = SkipDecision.Skip("Expected skip"); + + await executor.OnModuleEndAsync(moduleState, result); + await executor.OnModuleFailureAsync(moduleState, exception); + await executor.OnModuleSkippedAsync(moduleState, skipDecision); + + handler.Verify(x => x.OnModuleEndAsync(It.IsAny(), result), Times.Once); + handler.Verify(x => x.OnModuleFailureAsync(It.IsAny(), exception), Times.Once); + handler.Verify(x => x.OnModuleSkippedAsync(It.IsAny(), skipDecision), Times.Once); + } } diff --git a/test/ModularPipelines.UnitTests/Engine/RegistrationEventExecutorTests.cs b/test/ModularPipelines.UnitTests/Engine/RegistrationEventExecutorTests.cs index 26385557ead..8b645e43687 100644 --- a/test/ModularPipelines.UnitTests/Engine/RegistrationEventExecutorTests.cs +++ b/test/ModularPipelines.UnitTests/Engine/RegistrationEventExecutorTests.cs @@ -15,11 +15,11 @@ public async Task Later_Invocation_Cannot_Add_Module_Types() { var attributeEventService = new Mock(); attributeEventService - .Setup(service => service.GetRegistrationReceivers(It.IsAny())) + .Setup(service => service.GetRegistrationHandlers(It.IsAny())) .Returns([]); var executor = new RegistrationEventExecutor( attributeEventService.Object, - Mock.Of(), + Mock.Of(), new ModuleDependencyRegistry(), Mock.Of(), Mock.Of(), diff --git a/test/ModularPipelines.UnitTests/Engine/RunReportTests.cs b/test/ModularPipelines.UnitTests/Engine/RunReportTests.cs index aea1a4fc69e..fab86d8e79c 100644 --- a/test/ModularPipelines.UnitTests/Engine/RunReportTests.cs +++ b/test/ModularPipelines.UnitTests/Engine/RunReportTests.cs @@ -13,8 +13,9 @@ using ModularPipelines.DependencyInjection; using ModularPipelines.Distributed; using ModularPipelines.Distributed.Configuration; -using ModularPipelines.Enums; using ModularPipelines.Engine; +using ModularPipelines.Enums; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Extensions; using ModularPipelines.Helpers; @@ -106,7 +107,7 @@ private sealed class GenericModule : Module Task.FromResult(typeof(T).FullName); } - private sealed class ThrowingEndHook : IPipelineGlobalHooks + private sealed class ThrowingEndHook : IPipelineEventHandler { private const string RegisteredSecret = "hook-secret-value"; private readonly ISecretRegistry _secretRegistry; @@ -125,7 +126,7 @@ public Task OnPipelineEndAsync( } } - private sealed class MixedFailureEndHook : IPipelineGlobalHooks + private sealed class MixedFailureEndHook : IPipelineEventHandler { public Task OnPipelineEndAsync( IPipelineContext context, @@ -141,7 +142,7 @@ public Task OnPipelineEndAsync( } } - private sealed class WrappedFailureEndHook : IPipelineGlobalHooks + private sealed class WrappedFailureEndHook : IPipelineEventHandler { public Task OnPipelineEndAsync( IPipelineContext context, @@ -155,7 +156,7 @@ public Task OnPipelineEndAsync( } } - private sealed class DelayedEndHook : IPipelineGlobalHooks + private sealed class DelayedEndHook : IPipelineEventHandler { public static DateTimeOffset CompletedAt { get; private set; } @@ -2854,7 +2855,7 @@ public async Task PipelineLevelFailureIsPersistedAsFailed() RunReport = options.RunReport with { ReportPath = reportPath }, }); builder.AddModule(); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await Assert.ThrowsAsync( () => builder.RunAsync()); @@ -2895,7 +2896,7 @@ public async Task RunReportIncludesSuccessfulEndHookDuration() RunReport = options.RunReport with { ReportPath = reportPath }, }); builder.AddModule(); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); var summary = await builder.RunAsync(); var report = RunReportJsonSerializer.Deserialize( @@ -3112,7 +3113,7 @@ public async Task PipelineExceptionRetainsNonModuleAggregateBranches() RunReport = options.RunReport with { ReportPath = reportPath }, }); builder.AddModule(); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await Assert.ThrowsAsync(() => builder.RunAsync()); @@ -3153,7 +3154,7 @@ public async Task PipelineExceptionRetainsNonModuleWrapper() RunReport = options.RunReport with { ReportPath = reportPath }, }); builder.AddModule(); - builder.AddPipelineGlobalHooks(); + builder.AddPipelineEventHandler(); await Assert.ThrowsAsync( () => builder.RunAsync()); diff --git a/test/ModularPipelines.UnitTests/Execution/EngineCancellationTokenTests.cs b/test/ModularPipelines.UnitTests/Execution/EngineCancellationTokenTests.cs index bd48c8db181..b6ee5f7b63b 100644 --- a/test/ModularPipelines.UnitTests/Execution/EngineCancellationTokenTests.cs +++ b/test/ModularPipelines.UnitTests/Execution/EngineCancellationTokenTests.cs @@ -7,6 +7,7 @@ using ModularPipelines.Context.Domains.Shell; using ModularPipelines.Engine; using ModularPipelines.Engine.Execution; +using ModularPipelines.Events; using ModularPipelines.Exceptions; using ModularPipelines.Extensions; using ModularPipelines.Interfaces; @@ -318,7 +319,7 @@ private void RecordDependent(Type moduleType) } } - private sealed class ThrowingReadyHookReceiver : IModuleEventReceiver + private sealed class ThrowingReadyHookHandler : IModuleEventHandler { public Task OnModuleReadyAsync(IModuleHookContext context) { @@ -331,7 +332,7 @@ public Task OnModuleReadyAsync(IModuleHookContext context) } } - private sealed class IndependentlyCancellingReadyHookReceiver : IModuleEventReceiver + private sealed class IndependentlyCancellingReadyHookHandler : IModuleEventHandler { public Task OnModuleReadyAsync(IModuleHookContext context) { @@ -470,7 +471,7 @@ public async Task FailFast_Reports_DependencyFailed_When_ReadyHookThrows() var builder = TestPipelineBuilder.Create() .AddModule() .AddModule() - .AddModuleEventReceiver(); + .AddModuleEventHandler(); builder.ConfigurePipelineOptions(options => options with { ThrowOnPipelineFailure = true, @@ -502,7 +503,7 @@ public async Task FailFast_Preserves_Raw_ReadyHook_Failure_When_Dependent_Report .AddModule() .AddModule() .AddModule() - .AddModuleEventReceiver(); + .AddModuleEventHandler(); builder.ConfigurePipelineOptions(options => options with { ThrowOnPipelineFailure = true, @@ -593,7 +594,7 @@ public async Task FailFast_Wraps_Independent_ReadyHook_Cancellation_For_Dependen .AddModule() .AddModule() .AddModule() - .AddModuleEventReceiver(); + .AddModuleEventHandler(); builder.ConfigurePipelineOptions(options => options with { ThrowOnPipelineFailure = true, diff --git a/test/ModularPipelines.UnitTests/Helpers/CmdTests.cs b/test/ModularPipelines.UnitTests/Helpers/CmdTests.cs index 18eda5927c0..2bfb4465b6a 100644 --- a/test/ModularPipelines.UnitTests/Helpers/CmdTests.cs +++ b/test/ModularPipelines.UnitTests/Helpers/CmdTests.cs @@ -110,7 +110,7 @@ public async Task File_Options_Preserve_Explicit_Tool() await ModuleResultAssertions.AssertCommandOutput(moduleResult, TestConstants.TestString); } - private static ModularPipelines.FileSystem.File GetCmdTestFile(IModuleContext context) + private static ModularPipelines.FileSystem.FilePath GetCmdTestFile(IModuleContext context) { return context.Files.GetFile(Path.Combine( TestContext.OutputDirectory!, diff --git a/test/ModularPipelines.UnitTests/Hooks/DirectModuleHooksIntegrationTests.cs b/test/ModularPipelines.UnitTests/Hooks/DirectModuleHooksIntegrationTests.cs index d1034792816..fa205fb1e60 100644 --- a/test/ModularPipelines.UnitTests/Hooks/DirectModuleHooksIntegrationTests.cs +++ b/test/ModularPipelines.UnitTests/Hooks/DirectModuleHooksIntegrationTests.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.DependencyInjection; -using ModularPipelines.Attributes.Events; using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.Engine; using ModularPipelines.Enums; +using ModularPipelines.Events; using ModularPipelines.Extensions; using ModularPipelines.Interfaces; using ModularPipelines.Models; @@ -88,7 +88,7 @@ protected override Task OnBeforeExecuteAsync(IModuleContext context, Cancellatio private class Module1 : LoggingModule; private class Module2 : LoggingModule; - private sealed class RecordingModuleEventReceiver : IModuleEventReceiver + private sealed class RecordingModuleEventHandler : IModuleEventHandler { public Task OnModuleReadyAsync(IModuleHookContext context) { @@ -102,7 +102,7 @@ public Task OnModuleStartAsync(IModuleHookContext context) return Task.CompletedTask; } - public Task OnModuleEndAsync(IModuleHookContext context) + public Task OnModuleEndAsync(IModuleHookContext context, IModuleResult result) { AddLogEntry("Global:End"); return Task.CompletedTask; @@ -242,7 +242,7 @@ public async Task Global_Attribute_And_Module_Hooks_Have_Documented_Order() { var host = await TestPipelineBuilder.Create() .AddModule() - .AddModuleEventReceiver() + .AddModuleEventHandler() .BuildAsync(); await host.RunAsync();