diff --git a/src/ModularPipelines/Engine/Execution/DependencyWaiter.cs b/src/ModularPipelines/Engine/Execution/DependencyWaiter.cs
index ff47bf7603..7bde432762 100644
--- a/src/ModularPipelines/Engine/Execution/DependencyWaiter.cs
+++ b/src/ModularPipelines/Engine/Execution/DependencyWaiter.cs
@@ -27,7 +27,8 @@ public DependencyWaiter(
///
public async Task WaitForDependenciesAsync(ModuleState moduleState, IModuleScheduler scheduler, IServiceProvider scopedServiceProvider)
{
- var dependencies = ModuleDependencyResolver.GetDependencies(moduleState.ModuleType);
+ // Get both attribute-based and programmatic dependencies
+ var dependencies = GetAllDependencies(moduleState);
foreach (var (dependencyType, ignoreIfNotRegistered) in dependencies)
{
@@ -54,11 +55,29 @@ public async Task WaitForDependenciesAsync(ModuleState moduleState, IModuleSched
$"but '{dependencyType.Name}' has not been registered in the pipeline.\n\n" +
$"Suggestions:\n" +
$" 1. Add '.AddModule<{dependencyType.Name}>()' to your pipeline configuration before '.AddModule<{moduleState.ModuleType.Name}>()'\n" +
- $" 2. Use '[DependsOn<{dependencyType.Name}>(ignoreIfNotRegistered: true)]' if this dependency is optional\n" +
+ $" 2. Use 'deps.DependsOnOptional<{dependencyType.Name}>()' or '[DependsOn<{dependencyType.Name}>(IgnoreIfNotRegistered = true)]' if this dependency is optional\n" +
$" 3. Check for typos in the dependency type name\n" +
$" 4. Verify that '{dependencyType.Name}' is in a project referenced by your pipeline project";
throw new ModuleNotRegisteredException(message, null);
}
}
}
+
+ ///
+ /// Gets all dependencies for a module, combining attribute-based and programmatic dependencies.
+ ///
+ private static IEnumerable<(Type DependencyType, bool IgnoreIfNotRegistered)> GetAllDependencies(ModuleState moduleState)
+ {
+ // Attribute-based dependencies
+ foreach (var dep in ModuleDependencyResolver.GetDependencies(moduleState.ModuleType))
+ {
+ yield return dep;
+ }
+
+ // Programmatic dependencies from DeclareDependencies method
+ foreach (var dep in ModuleDependencyResolver.GetProgrammaticDependencies(moduleState.Module))
+ {
+ yield return dep;
+ }
+ }
}
diff --git a/src/ModularPipelines/Engine/ModuleDependencyResolver.cs b/src/ModularPipelines/Engine/ModuleDependencyResolver.cs
index 781216d491..16fd7015fb 100644
--- a/src/ModularPipelines/Engine/ModuleDependencyResolver.cs
+++ b/src/ModularPipelines/Engine/ModuleDependencyResolver.cs
@@ -1,16 +1,24 @@
+using System.Collections.Concurrent;
using System.Reflection;
using ModularPipelines.Attributes;
using ModularPipelines.Engine.Dependencies;
+using ModularPipelines.Enums;
using ModularPipelines.Extensions;
+using ModularPipelines.Models;
using ModularPipelines.Modules;
namespace ModularPipelines.Engine;
///
-/// Resolves module dependencies by inspecting DependsOn attributes.
+/// Resolves module dependencies by inspecting DependsOn attributes and programmatic declarations.
///
internal static class ModuleDependencyResolver
{
+ ///
+ /// Cache for GetDeclaredDependencies method lookups to avoid repeated reflection.
+ ///
+ private static readonly ConcurrentDictionary GetDeclaredDependenciesMethodCache = new();
+
///
/// Gets all dependencies declared on a module type via DependsOn attributes.
/// This overload only handles DependsOnAttribute, not DependsOnAllModulesInheritingFromAttribute.
@@ -65,6 +73,37 @@ internal static class ModuleDependencyResolver
return GetDependencies(module.GetType());
}
+ ///
+ /// Gets programmatic dependencies declared via DeclareDependencies method on a module instance.
+ ///
+ /// The module instance to get programmatic dependencies from.
+ /// An enumerable of dependency tuples (DependencyType, IgnoreIfNotRegistered).
+ public static IEnumerable<(Type DependencyType, bool IgnoreIfNotRegistered)> GetProgrammaticDependencies(IModule module)
+ {
+ // Use cached reflection lookup for GetDeclaredDependencies method
+ var moduleType = module.GetType();
+ var method = GetDeclaredDependenciesMethodCache.GetOrAdd(moduleType, type =>
+ type.GetMethod("GetDeclaredDependencies",
+ BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
+
+ if (method == null)
+ {
+ yield break;
+ }
+
+ var dependencies = method.Invoke(module, null) as IReadOnlyList;
+
+ if (dependencies == null)
+ {
+ yield break;
+ }
+
+ foreach (var dep in dependencies)
+ {
+ yield return (dep.ModuleType, dep.IgnoreIfNotRegistered);
+ }
+ }
+
///
/// Gets all dependencies declared on a module type via DependsOn attributes,
/// including both static (attribute-based) and dynamic (runtime-added) dependencies.
@@ -89,4 +128,39 @@ internal static class ModuleDependencyResolver
}
}
}
+
+ ///
+ /// Gets all dependencies declared on a module instance, including:
+ /// - Static dependencies from DependsOn attributes
+ /// - Programmatic dependencies from DeclareDependencies method
+ /// - Dynamic dependencies from the registry
+ ///
+ public static IEnumerable<(Type DependencyType, bool IgnoreIfNotRegistered)> GetAllDependencies(
+ IModule module,
+ IEnumerable availableModuleTypes,
+ IModuleDependencyRegistry? dynamicRegistry = null)
+ {
+ var moduleType = module.GetType();
+
+ // Static dependencies from attributes
+ foreach (var dep in GetDependencies(moduleType, availableModuleTypes))
+ {
+ yield return dep;
+ }
+
+ // Programmatic dependencies from DeclareDependencies method
+ foreach (var dep in GetProgrammaticDependencies(module))
+ {
+ yield return dep;
+ }
+
+ // Dynamic dependencies from registration
+ if (dynamicRegistry != null)
+ {
+ foreach (var dynamicDep in dynamicRegistry.GetDynamicDependencies(moduleType))
+ {
+ yield return (dynamicDep, false);
+ }
+ }
+ }
}
diff --git a/src/ModularPipelines/Engine/ModuleScheduler.cs b/src/ModularPipelines/Engine/ModuleScheduler.cs
index efd9dbe220..9002daf955 100644
--- a/src/ModularPipelines/Engine/ModuleScheduler.cs
+++ b/src/ModularPipelines/Engine/ModuleScheduler.cs
@@ -155,7 +155,8 @@ public void InitializeModules(IEnumerable modules)
}
// Use the overload that includes dynamic dependencies from registration events
- var dependencies = ModuleDependencyResolver.GetAllDependencies(moduleType, availableModuleTypes, _dependencyRegistry);
+ // and programmatic dependencies from DeclareDependencies method
+ var dependencies = ModuleDependencyResolver.GetAllDependencies(state.Module, availableModuleTypes, _dependencyRegistry);
foreach (var (dependencyType, ignoreIfNotRegistered) in dependencies)
{
@@ -202,7 +203,8 @@ public void AddModule(IModule module)
{
// Resolve dependencies inside write lock to prevent race conditions
// where _moduleStates could change between resolution and processing
- var dependencies = ModuleDependencyResolver.GetDependencies(moduleType);
+ var dependencies = ModuleDependencyResolver.GetDependencies(moduleType)
+ .Concat(ModuleDependencyResolver.GetProgrammaticDependencies(module));
foreach (var (dependencyType, ignoreIfNotRegistered) in dependencies)
{
diff --git a/src/ModularPipelines/Enums/DependencyType.cs b/src/ModularPipelines/Enums/DependencyType.cs
new file mode 100644
index 0000000000..6b9b05f098
--- /dev/null
+++ b/src/ModularPipelines/Enums/DependencyType.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace ModularPipelines.Enums;
+
+///
+/// Defines the type of dependency between modules.
+///
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum DependencyType
+{
+ ///
+ /// Required dependency. The dependent module will fail if this dependency is not registered.
+ ///
+ Required = 0,
+
+ ///
+ /// Optional dependency. The dependent module will not fail if this dependency is not registered.
+ ///
+ Optional = 1,
+
+ ///
+ /// Lazy dependency. Behaves the same as for dependency resolution purposes -
+ /// the dependency is optional and will not fail if not registered. This is a semantic marker to
+ /// indicate intent that the dependency may be awaited on-demand rather than required upfront.
+ ///
+ Lazy = 2,
+
+ ///
+ /// Conditional dependency. Used when a dependency is added based on a runtime condition via
+ /// . For dependency resolution,
+ /// this behaves as a required dependency (will fail if condition is true and dependency is not registered).
+ ///
+ Conditional = 3,
+}
diff --git a/src/ModularPipelines/Models/DeclaredDependency.cs b/src/ModularPipelines/Models/DeclaredDependency.cs
new file mode 100644
index 0000000000..d443da11dc
--- /dev/null
+++ b/src/ModularPipelines/Models/DeclaredDependency.cs
@@ -0,0 +1,39 @@
+using ModularPipelines.Enums;
+
+namespace ModularPipelines.Models;
+
+///
+/// Represents a dependency declared programmatically via .
+///
+/// The type of the module being depended on.
+/// The kind of dependency (Required, Optional, Lazy, Conditional).
+/// Whether to ignore this dependency if not registered.
+public readonly record struct DeclaredDependency(
+ Type ModuleType,
+ DependencyType Kind,
+ bool IgnoreIfNotRegistered)
+{
+ ///
+ /// Creates a required dependency.
+ ///
+ public static DeclaredDependency Required(Type type) =>
+ new(type, Enums.DependencyType.Required, false);
+
+ ///
+ /// Creates an optional dependency.
+ ///
+ public static DeclaredDependency Optional(Type type) =>
+ new(type, Enums.DependencyType.Optional, true);
+
+ ///
+ /// Creates a lazy dependency.
+ ///
+ public static DeclaredDependency Lazy(Type type) =>
+ new(type, Enums.DependencyType.Lazy, true);
+
+ ///
+ /// Creates a conditional dependency.
+ ///
+ public static DeclaredDependency Conditional(Type type) =>
+ new(type, Enums.DependencyType.Conditional, false);
+}
diff --git a/src/ModularPipelines/Modules/DependencyDeclaration.cs b/src/ModularPipelines/Modules/DependencyDeclaration.cs
new file mode 100644
index 0000000000..bb71374b4d
--- /dev/null
+++ b/src/ModularPipelines/Modules/DependencyDeclaration.cs
@@ -0,0 +1,111 @@
+using ModularPipelines.Enums;
+using ModularPipelines.Models;
+
+namespace ModularPipelines.Modules;
+
+///
+/// Implementation of for collecting programmatic dependencies.
+///
+///
+///
+/// Thread Safety: This class is not thread-safe. It is designed to be used
+/// during the synchronous method call
+/// and should not be accessed from multiple threads.
+///
+///
+internal sealed class DependencyDeclaration : IDependencyDeclaration
+{
+ private readonly List _dependencies = new();
+
+ ///
+ /// Gets the declared dependencies.
+ ///
+ public IReadOnlyList Dependencies => _dependencies;
+
+ ///
+ public IDependencyDeclaration DependsOn() where TModule : IModule
+ {
+ return DependsOn(typeof(TModule));
+ }
+
+ ///
+ public IDependencyDeclaration DependsOn(Type moduleType)
+ {
+ ValidateModuleType(moduleType);
+ _dependencies.Add(DeclaredDependency.Required(moduleType));
+ return this;
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnOptional() where TModule : IModule
+ {
+ return DependsOnOptional(typeof(TModule));
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnOptional(Type moduleType)
+ {
+ ValidateModuleType(moduleType);
+ _dependencies.Add(DeclaredDependency.Optional(moduleType));
+ return this;
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnIf(bool condition) where TModule : IModule
+ {
+ return DependsOnIf(typeof(TModule), condition);
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnIf(Func predicate) where TModule : IModule
+ {
+ ArgumentNullException.ThrowIfNull(predicate);
+ return DependsOnIf(typeof(TModule), predicate());
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnIf(Type moduleType, bool condition)
+ {
+ if (!condition)
+ {
+ return this;
+ }
+
+ ValidateModuleType(moduleType);
+ _dependencies.Add(DeclaredDependency.Conditional(moduleType));
+ return this;
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnIf(Type moduleType, Func predicate)
+ {
+ ArgumentNullException.ThrowIfNull(predicate);
+ return DependsOnIf(moduleType, predicate());
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnLazy() where TModule : IModule
+ {
+ return DependsOnLazy(typeof(TModule));
+ }
+
+ ///
+ public IDependencyDeclaration DependsOnLazy(Type moduleType)
+ {
+ ValidateModuleType(moduleType);
+ _dependencies.Add(DeclaredDependency.Lazy(moduleType));
+ return this;
+ }
+
+ private static void ValidateModuleType(Type moduleType)
+ {
+ ArgumentNullException.ThrowIfNull(moduleType);
+
+ if (!moduleType.IsAssignableTo(typeof(IModule)))
+ {
+ throw new ArgumentException(
+ $"{moduleType.FullName} is not a Module (does not implement IModule)",
+ nameof(moduleType));
+ }
+ }
+}
diff --git a/src/ModularPipelines/Modules/IDependencyDeclaration.cs b/src/ModularPipelines/Modules/IDependencyDeclaration.cs
new file mode 100644
index 0000000000..2842d1e3a2
--- /dev/null
+++ b/src/ModularPipelines/Modules/IDependencyDeclaration.cs
@@ -0,0 +1,167 @@
+namespace ModularPipelines.Modules;
+
+///
+/// Interface for declaring module dependencies programmatically at runtime.
+///
+///
+///
+/// This interface provides methods for declaring dependencies dynamically, complementing
+/// the static approach.
+///
+///
+/// Dependencies declared via this interface are combined with attribute-based dependencies.
+///
+///
+///
+///
+/// public class ApiModule : Module<ApiResult>
+/// {
+/// protected override void DeclareDependencies(IDependencyDeclaration deps)
+/// {
+/// // Required dependency
+/// deps.DependsOn<DatabaseModule>();
+///
+/// // Optional dependency (doesn't fail if not registered)
+/// deps.DependsOnOptional<CachingModule>();
+///
+/// // Conditional dependency
+/// if (Environment.IsProduction)
+/// {
+/// deps.DependsOn<PerformanceMonitoringModule>();
+/// }
+///
+/// // Conditional with predicate
+/// deps.DependsOnIf<HeavyProcessingModule>(() => ShouldRunHeavyProcessing);
+/// }
+/// }
+///
+///
+public interface IDependencyDeclaration
+{
+ ///
+ /// Declares a required dependency on the specified module type.
+ ///
+ /// The type of module to depend on.
+ /// This instance for method chaining.
+ ///
+ /// Thrown during pipeline execution if the dependency is not registered.
+ ///
+ IDependencyDeclaration DependsOn() where TModule : IModule;
+
+ ///
+ /// Declares a required dependency on the specified module type.
+ ///
+ /// The type of module to depend on.
+ /// This instance for method chaining.
+ ///
+ /// Thrown if the type does not implement .
+ ///
+ ///
+ /// Thrown during pipeline execution if the dependency is not registered.
+ ///
+ IDependencyDeclaration DependsOn(Type moduleType);
+
+ ///
+ /// Declares an optional dependency on the specified module type.
+ /// Unlike , this will not fail if the dependency is not registered.
+ ///
+ /// The type of module to depend on.
+ /// This instance for method chaining.
+ IDependencyDeclaration DependsOnOptional() where TModule : IModule;
+
+ ///
+ /// Declares an optional dependency on the specified module type.
+ /// Unlike , this will not fail if the dependency is not registered.
+ ///
+ /// The type of module to depend on.
+ /// This instance for method chaining.
+ ///
+ /// Thrown if the type does not implement .
+ ///
+ IDependencyDeclaration DependsOnOptional(Type moduleType);
+
+ ///
+ /// Declares a conditional dependency that is only active if the condition is true.
+ ///
+ /// The type of module to depend on.
+ /// The condition that determines whether the dependency is active.
+ /// This instance for method chaining.
+ ///
+ /// If the condition is false, the dependency is not added at all.
+ ///
+ IDependencyDeclaration DependsOnIf(bool condition) where TModule : IModule;
+
+ ///
+ /// Declares a conditional dependency that is only active if the predicate returns true.
+ ///
+ /// The type of module to depend on.
+ /// The predicate that determines whether the dependency is active.
+ /// This instance for method chaining.
+ ///
+ /// The predicate is evaluated immediately when this method is called.
+ /// If it returns false, the dependency is not added at all.
+ ///
+ IDependencyDeclaration DependsOnIf(Func predicate) where TModule : IModule;
+
+ ///
+ /// Declares a conditional dependency that is only active if the condition is true.
+ ///
+ /// The type of module to depend on.
+ /// The condition that determines whether the dependency is active.
+ /// This instance for method chaining.
+ ///
+ /// Thrown if the type does not implement .
+ ///
+ IDependencyDeclaration DependsOnIf(Type moduleType, bool condition);
+
+ ///
+ /// Declares a conditional dependency that is only active if the predicate returns true.
+ ///
+ /// The type of module to depend on.
+ /// The predicate that determines whether the dependency is active.
+ /// This instance for method chaining.
+ ///
+ /// Thrown if the type does not implement .
+ ///
+ IDependencyDeclaration DependsOnIf(Type moduleType, Func predicate);
+
+ ///
+ /// Declares a lazy dependency - an optional dependency intended to be awaited on-demand.
+ ///
+ /// The type of module to depend on.
+ /// This instance for method chaining.
+ ///
+ ///
+ /// Lazy dependencies behave the same as for
+ /// dependency resolution purposes - the dependency is optional and will not fail if not
+ /// registered. This is a semantic marker to indicate intent that the dependency may be
+ /// awaited on-demand rather than required upfront.
+ ///
+ ///
+ /// Important: The lazy module will still execute during normal pipeline
+ /// scheduling if it is registered. It does NOT defer execution until explicitly awaited.
+ /// The "lazy" designation indicates the dependency relationship is optional and the result
+ /// may be consumed on-demand, but does not affect when the module runs.
+ ///
+ ///
+ /// Use this to express intent for optional processing that may or may not be awaited.
+ ///
+ ///
+ IDependencyDeclaration DependsOnLazy() where TModule : IModule;
+
+ ///
+ /// Declares a lazy dependency - an optional dependency intended to be awaited on-demand.
+ ///
+ /// The type of module to depend on.
+ /// This instance for method chaining.
+ ///
+ ///
+ /// Lazy dependencies behave the same as for
+ /// dependency resolution purposes. See for full details.
+ ///
+ ///
+ ///
+ /// Thrown if the type does not implement .
+ ///
+ IDependencyDeclaration DependsOnLazy(Type moduleType);
+}
diff --git a/src/ModularPipelines/Modules/Module.cs b/src/ModularPipelines/Modules/Module.cs
index 0d5e7dc71a..ee43f9b688 100644
--- a/src/ModularPipelines/Modules/Module.cs
+++ b/src/ModularPipelines/Modules/Module.cs
@@ -9,7 +9,9 @@ namespace ModularPipelines.Modules;
///
/// The type of result returned by the module.
///
+///
/// Modules can optionally implement behavior interfaces to customize execution:
+///
///
/// - - Define skip conditions
/// - - Set execution timeout
@@ -18,7 +20,32 @@ namespace ModularPipelines.Modules;
/// - - Add before/after execution hooks
/// - - Run even when pipeline fails
///
+///
+/// Dependencies can be declared in two ways:
+///
+///
+/// - Statically via attributes
+/// - Dynamically by overriding
+///
///
+///
+///
+/// public class ApiModule : Module<ApiResult>
+/// {
+/// protected override void DeclareDependencies(IDependencyDeclaration deps)
+/// {
+/// deps.DependsOn<DatabaseModule>();
+/// deps.DependsOnOptional<CachingModule>();
+/// deps.DependsOnIf<MonitoringModule>(Environment.IsProduction);
+/// }
+///
+/// public override async Task<ApiResult?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+/// {
+/// // Implementation
+/// }
+/// }
+///
+///
public abstract class Module : IModule
{
internal TaskCompletionSource> CompletionSource { get; } = new();
@@ -26,6 +53,46 @@ public abstract class Module : IModule
///
Type IModule.ResultType => typeof(T);
+ ///
+ /// Declares dependencies programmatically at runtime.
+ /// Override this method to add dynamic or conditional dependencies.
+ ///
+ /// The dependency declaration builder.
+ ///
+ ///
+ /// Dependencies declared here are combined with attribute-based dependencies
+ /// from .
+ ///
+ ///
+ /// This method is called once during module initialization, before execution begins.
+ ///
+ ///
+ ///
+ ///
+ /// protected override void DeclareDependencies(IDependencyDeclaration deps)
+ /// {
+ /// deps.DependsOn<RequiredModule>();
+ /// deps.DependsOnOptional<OptionalModule>();
+ /// deps.DependsOnIf<ConditionalModule>(SomeCondition);
+ /// deps.DependsOnLazy<HeavyModule>();
+ /// }
+ ///
+ ///
+ protected virtual void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ // Default implementation does nothing - dependencies are declared via attributes only
+ }
+
+ ///
+ /// Internal method to collect declared dependencies.
+ ///
+ internal IReadOnlyList GetDeclaredDependencies()
+ {
+ var declaration = new DependencyDeclaration();
+ DeclareDependencies(declaration);
+ return declaration.Dependencies;
+ }
+
///
/// Executes the module's core logic.
///
@@ -34,5 +101,8 @@ public abstract class Module : IModule
/// The result of the module execution, or null.
public abstract Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken);
+ ///
+ /// Gets an awaiter for this module's result.
+ ///
public TaskAwaiter> GetAwaiter() => CompletionSource.Task.GetAwaiter();
}
diff --git a/test/ModularPipelines.UnitTests/DynamicDependencyDeclarationTests.cs b/test/ModularPipelines.UnitTests/DynamicDependencyDeclarationTests.cs
new file mode 100644
index 0000000000..3da64ba78f
--- /dev/null
+++ b/test/ModularPipelines.UnitTests/DynamicDependencyDeclarationTests.cs
@@ -0,0 +1,541 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Context;
+using ModularPipelines.Enums;
+using ModularPipelines.Exceptions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+using ModularPipelines.TestHelpers;
+using Status = ModularPipelines.Enums.Status;
+
+namespace ModularPipelines.UnitTests;
+
+///
+/// Tests for the dynamic dependency declaration feature introduced in Issue #1870.
+///
+public class DynamicDependencyDeclarationTests : TestBase
+{
+ #region Helper Modules
+
+ ///
+ /// A basic module with no dependencies.
+ ///
+ private class BaseModule : SimpleTestModule
+ {
+ protected override string Result => "base";
+ }
+
+ ///
+ /// A module that others can optionally depend on.
+ ///
+ private class OptionalDependencyModule : SimpleTestModule
+ {
+ protected override string Result => "optional";
+ }
+
+ ///
+ /// A module for testing lazy dependencies.
+ ///
+ private class LazyModule : SimpleTestModule
+ {
+ protected override string Result => "lazy";
+ }
+
+ ///
+ /// A module for testing conditional dependencies.
+ ///
+ private class ConditionalModule : SimpleTestModule
+ {
+ protected override string Result => "conditional";
+ }
+
+ ///
+ /// A module that declares a required dependency programmatically.
+ ///
+ private class ModuleWithProgrammaticDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOn();
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "programmatic";
+ }
+ }
+
+ ///
+ /// A module that declares an optional dependency programmatically.
+ ///
+ private class ModuleWithOptionalDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOnOptional();
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "optional-dep";
+ }
+ }
+
+ ///
+ /// A module that declares a conditional dependency that is active.
+ ///
+ private class ModuleWithActiveConditionalDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOnIf(true);
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "conditional-active";
+ }
+ }
+
+ ///
+ /// A module that declares a conditional dependency that is inactive.
+ ///
+ private class ModuleWithInactiveConditionalDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOnIf(false);
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "conditional-inactive";
+ }
+ }
+
+ ///
+ /// A module that declares a conditional dependency using a predicate.
+ ///
+ private class ModuleWithPredicateConditionalDependency : Module
+ {
+ public static bool ShouldDependOnConditional { get; set; } = true;
+
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOnIf(() => ShouldDependOnConditional);
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "predicate-conditional";
+ }
+ }
+
+ ///
+ /// A module that declares a lazy dependency.
+ ///
+ private class ModuleWithLazyDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOnLazy();
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "lazy-dep";
+ }
+ }
+
+ ///
+ /// A module that combines both attribute and programmatic dependencies.
+ ///
+ [ModularPipelines.Attributes.DependsOn]
+ private class ModuleWithBothAttributeAndProgrammaticDependencies : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOnOptional();
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "combined";
+ }
+ }
+
+ ///
+ /// A module that chains multiple dependency declarations.
+ ///
+ private class ModuleWithChainedDependencies : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOn()
+ .DependsOnOptional()
+ .DependsOnLazy();
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "chained";
+ }
+ }
+
+ ///
+ /// A module that depends on an unregistered required dependency (should fail).
+ ///
+ private class ModuleWithMissingRequiredDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOn(); // BaseModule not registered
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "missing-dep";
+ }
+ }
+
+ ///
+ /// A module that uses DependsOn with Type parameter.
+ ///
+ private class ModuleWithTypeDependency : Module
+ {
+ protected override void DeclareDependencies(IDependencyDeclaration deps)
+ {
+ deps.DependsOn(typeof(BaseModule));
+ }
+
+ public override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return "type-dep";
+ }
+ }
+
+ #endregion
+
+ #region Required Dependency Tests
+
+ [Test]
+ public async Task Programmatic_Required_Dependency_Works_When_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Programmatic_Required_Dependency_Throws_When_Not_Registered()
+ {
+ await Assert.That(async () => await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .ExecutePipelineAsync())
+ .ThrowsException();
+ }
+
+ [Test]
+ public async Task Programmatic_Type_Dependency_Works()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ #endregion
+
+ #region Optional Dependency Tests
+
+ [Test]
+ public async Task Optional_Dependency_Works_When_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Optional_Dependency_Does_Not_Fail_When_Not_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ #endregion
+
+ #region Conditional Dependency Tests
+
+ [Test]
+ public async Task Conditional_Dependency_Works_When_Condition_True_And_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Conditional_Dependency_Throws_When_Condition_True_And_Not_Registered()
+ {
+ await Assert.That(async () => await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .ExecutePipelineAsync())
+ .ThrowsException();
+ }
+
+ [Test]
+ public async Task Conditional_Dependency_Not_Added_When_Condition_False()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ [TUnit.Core.NotInParallel(Order = 1)]
+ public async Task Conditional_Predicate_Dependency_Works_When_Predicate_Returns_True()
+ {
+ ModuleWithPredicateConditionalDependency.ShouldDependOnConditional = true;
+
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ [TUnit.Core.NotInParallel(Order = 2)]
+ public async Task Conditional_Predicate_Dependency_Not_Added_When_Predicate_Returns_False()
+ {
+ ModuleWithPredicateConditionalDependency.ShouldDependOnConditional = false;
+
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ #endregion
+
+ #region Lazy Dependency Tests
+
+ [Test]
+ public async Task Lazy_Dependency_Does_Not_Fail_When_Not_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Lazy_Dependency_Works_When_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ #endregion
+
+ #region Combined Dependency Tests
+
+ [Test]
+ public async Task Combined_Attribute_And_Programmatic_Dependencies_Work()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Combined_Dependencies_Work_With_Only_Attribute_Dependency_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Chained_Dependency_Declarations_Work()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ [Test]
+ public async Task Chained_Dependency_Declarations_Work_With_Only_Required_Registered()
+ {
+ var pipelineSummary = await TestPipelineHostBuilder.Create()
+ .AddModule()
+ .AddModule()
+ .ExecutePipelineAsync();
+
+ await Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful);
+ }
+
+ #endregion
+
+ #region DependencyDeclaration Unit Tests
+
+ [Test]
+ public async Task DependencyDeclaration_DependsOn_Returns_Same_Instance_For_Chaining()
+ {
+ var declaration = new DependencyDeclaration();
+
+ var result = declaration.DependsOn();
+
+ await Assert.That(result).IsSameReferenceAs(declaration);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_DependsOnOptional_Returns_Same_Instance_For_Chaining()
+ {
+ var declaration = new DependencyDeclaration();
+
+ var result = declaration.DependsOnOptional();
+
+ await Assert.That(result).IsSameReferenceAs(declaration);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_DependsOnIf_Returns_Same_Instance_For_Chaining()
+ {
+ var declaration = new DependencyDeclaration();
+
+ var result = declaration.DependsOnIf(true);
+
+ await Assert.That(result).IsSameReferenceAs(declaration);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_DependsOnLazy_Returns_Same_Instance_For_Chaining()
+ {
+ var declaration = new DependencyDeclaration();
+
+ var result = declaration.DependsOnLazy();
+
+ await Assert.That(result).IsSameReferenceAs(declaration);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_Throws_For_Non_Module_Type()
+ {
+ var declaration = new DependencyDeclaration();
+
+ await Assert.That(() => declaration.DependsOn(typeof(string)))
+ .ThrowsException()
+ .And.HasMessageContaining("is not a Module");
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_Required_Has_Correct_DependencyType()
+ {
+ var declaration = new DependencyDeclaration();
+ declaration.DependsOn();
+
+ var deps = declaration.Dependencies;
+
+ await Assert.That(deps).HasCount().EqualTo(1);
+ await Assert.That(deps[0].Kind).IsEqualTo(DependencyType.Required);
+ await Assert.That(deps[0].IgnoreIfNotRegistered).IsEqualTo(false);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_Optional_Has_Correct_DependencyType()
+ {
+ var declaration = new DependencyDeclaration();
+ declaration.DependsOnOptional();
+
+ var deps = declaration.Dependencies;
+
+ await Assert.That(deps).HasCount().EqualTo(1);
+ await Assert.That(deps[0].Kind).IsEqualTo(DependencyType.Optional);
+ await Assert.That(deps[0].IgnoreIfNotRegistered).IsEqualTo(true);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_Lazy_Has_Correct_DependencyType()
+ {
+ var declaration = new DependencyDeclaration();
+ declaration.DependsOnLazy();
+
+ var deps = declaration.Dependencies;
+
+ await Assert.That(deps).HasCount().EqualTo(1);
+ await Assert.That(deps[0].Kind).IsEqualTo(DependencyType.Lazy);
+ await Assert.That(deps[0].IgnoreIfNotRegistered).IsEqualTo(true);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_Conditional_Has_Correct_DependencyType()
+ {
+ var declaration = new DependencyDeclaration();
+ declaration.DependsOnIf(true);
+
+ var deps = declaration.Dependencies;
+
+ await Assert.That(deps).HasCount().EqualTo(1);
+ await Assert.That(deps[0].Kind).IsEqualTo(DependencyType.Conditional);
+ await Assert.That(deps[0].IgnoreIfNotRegistered).IsEqualTo(false);
+ }
+
+ [Test]
+ public async Task DependencyDeclaration_Conditional_False_Does_Not_Add_Dependency()
+ {
+ var declaration = new DependencyDeclaration();
+ declaration.DependsOnIf(false);
+
+ var deps = declaration.Dependencies;
+
+ await Assert.That(deps).HasCount().EqualTo(0);
+ }
+
+ #endregion
+}