Skip to content
Merged
131 changes: 129 additions & 2 deletions docs/docs/how-to/execution-and-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ sidebar_position: 3

# Execution and Dependencies

The default behaviour is for modules to run in parallel, to speed up a pipeline as much as possible.
The default behaviour is for modules to run in parallel, to speed up a pipeline as much as possible.

If you don't want a particular module to start until another one has finished, then you simply add a `[DependsOn<TModule>]` attribute to your module class.

Expand All @@ -17,4 +17,131 @@ public class Module2 : Module
{
...
}
```
```

## Required vs Optional Dependencies

By default, dependencies declared with `[DependsOn<T>]` are **required**. This means:

1. **Auto-registration**: If the dependency module is not explicitly registered, ModularPipelines will automatically register it for you
2. **Validation**: The pipeline validates that all required dependencies can be resolved before execution

```csharp
// Required dependency (default)
// Module1 will be auto-registered if not explicitly added
[DependsOn<Module1>]
public class Module2 : Module<string>
{
protected override async Task<string?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
// Safe to call - Module1 is guaranteed to be registered
var result = await context.GetModule<Module1>();
return result.Value;
}
}
```

### Auto-Registration

When you declare a required dependency, you don't need to explicitly register it:

```csharp
await PipelineHostBuilder.Create()
.AddModule<Module2>() // Module1 is auto-registered because Module2 depends on it
.ExecutePipelineAsync();
```

This simplifies pipeline configuration and ensures all required dependencies are always present. Auto-registration also handles transitive dependencies - if Module1 depends on Module0, both will be auto-registered.

## Optional Dependencies

Use `Optional = true` when a dependency may or may not be present:

```csharp
// Optional dependency - won't be auto-registered
[DependsOn<Module1>(Optional = true)]
public class Module2 : Module<string>
{
protected override async Task<string?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
// Use GetModuleIfRegistered for optional dependencies
var module1 = context.GetModuleIfRegistered<Module1>();

if (module1 != null)
{
var result = await module1;
return $"Got result: {result.Value}";
}

return "Module1 not available";
}
}
```

Optional dependencies are useful when:

- A module can work with or without another module's output
- You're using category filters and some dependencies may be excluded
- You want conditional behavior based on what modules are registered

### Category Filters and Optional Dependencies

When using `RunOnlyCategories` to filter which modules run, dependencies in other categories may not execute. Mark such dependencies as optional:

```csharp
[ModuleCategory("test")]
[DependsOn<CompileModule>(Optional = true)] // CompileModule is in "compile" category
public class TestModule : Module<string>
{
protected override async Task<string?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var compile = context.GetModuleIfRegistered<CompileModule>();

if (compile == null)
{
// CompileModule was filtered out - handle gracefully
return "Running tests without compile";
}

var result = await compile;
return result.IsSkipped
? "Compile was skipped"
: $"Compile result: {result.Value}";
}
}
```

## Accessing Dependency Results

Use `GetModule<T>()` for required dependencies - it throws if the module is not registered:

```csharp
var result = await context.GetModule<Module1>();
```

Use `GetModuleIfRegistered<T>()` for optional dependencies - it returns null if not registered:

```csharp
var module = context.GetModuleIfRegistered<Module1>();
if (module != null)
{
var result = await module;
// Use the result
}
```

## Programmatic Dependencies

You can also declare dependencies programmatically by overriding `DeclareDependencies`:

```csharp
public class Module2 : Module<string>
{
protected override void DeclareDependencies(IDependencyDeclaration deps)
{
deps.DependsOn<Module1>(); // Required
deps.DependsOnOptional<Module3>(); // Optional
deps.DependsOnIf<Module4>(someCondition); // Conditional
}
}
```
2 changes: 1 addition & 1 deletion src/ModularPipelines.Examples/Modules/FailedModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace ModularPipelines.Examples.Modules;

[DependsOn<SuccessModule3>(IgnoreIfNotRegistered = true)]
[DependsOn<SuccessModule3>(Optional = true)]
public class FailedModule : Module<IDictionary<string, object>?>
{
/// <inheritdoc/>
Expand Down
101 changes: 99 additions & 2 deletions src/ModularPipelines/Attributes/DependsOnAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,51 @@

namespace ModularPipelines.Attributes;

/// <summary>
/// Declares a dependency on another module. The current module will not execute until the dependency has completed.
/// </summary>
/// <remarks>
/// <para>
/// By default, dependencies are <b>required</b>. Required dependencies are automatically registered
/// if not explicitly added to the pipeline. This ensures all dependencies are always present.
/// </para>
/// <para>
/// Use <see cref="Optional"/> = <c>true</c> for dependencies that may or may not be present.
/// Optional dependencies are not auto-registered and won't cause validation errors if missing.
/// </para>
/// <example>
/// <code>
/// // Required dependency - Module1 will be auto-registered if not present
/// [DependsOn&lt;Module1&gt;]
/// public class Module2 : Module&lt;string&gt; { }
///
/// // Optional dependency - Module1 won't be auto-registered
/// [DependsOn&lt;Module1&gt;(Optional = true)]
/// public class Module3 : Module&lt;string&gt;
/// {
/// protected override async Task&lt;string?&gt; ExecuteAsync(IModuleContext context, CancellationToken ct)
/// {
/// // Use GetModuleIfRegistered for optional dependencies
/// var module1 = context.GetModuleIfRegistered&lt;Module1&gt;();
/// if (module1 != null)
/// {
/// var result = await module1;
/// return result.Value;
/// }
/// return "Module1 not available";
/// }
/// }
/// </code>
/// </example>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = true)]
public class DependsOnAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.
/// </summary>
/// <param name="type">The type of module this module depends on.</param>
/// <exception cref="InvalidModuleTypeException">Thrown when the type does not implement <see cref="IModule"/>.</exception>
[Obsolete("Use the generic DependsOnAttribute<TModule> instead for compile-time type safety. This constructor will be removed in a future version.")]
public DependsOnAttribute(Type type)
{
Expand All @@ -25,16 +67,71 @@ internal DependsOnAttribute(Type type, bool skipValidation)
Type = type;
}

/// <summary>
/// Gets the type of module this module depends on.
/// </summary>
public Type Type { get; }

public bool IgnoreIfNotRegistered { get; set; }
/// <summary>
/// Gets or sets whether this dependency is optional.
/// </summary>
/// <remarks>
/// <para>
/// When <c>false</c> (default), the dependency is <b>required</b>:
/// </para>
/// <list type="bullet">
/// <item>The dependency module will be <b>auto-registered</b> if not explicitly added to the pipeline</item>
/// <item>Use <c>GetModule&lt;T&gt;()</c> to access the dependency - it is guaranteed to be present</item>
/// </list>
/// <para>
/// When <c>true</c>, the dependency is <b>optional</b>:
/// </para>
/// <list type="bullet">
/// <item>The dependency module will <b>not</b> be auto-registered</item>
/// <item>Use <c>GetModuleIfRegistered&lt;T&gt;()</c> to safely check if the dependency exists</item>
/// <item>Useful when using category filters where dependencies may be excluded</item>
/// </list>
/// </remarks>
/// <value>
/// <c>true</c> if the dependency is optional; <c>false</c> if the dependency is required. Default is <c>false</c>.
/// </value>
public bool Optional { get; set; } = false;
}

/// <summary>
/// Declares a dependency on another module of type <typeparamref name="TModule"/>.
/// The current module will not execute until the dependency has completed.
/// </summary>
/// <typeparam name="TModule">The type of module this module depends on.</typeparam>
/// <remarks>
/// <para>
/// By default, dependencies are <b>required</b>. Required dependencies are automatically registered
/// if not explicitly added to the pipeline. This ensures all dependencies are always present.
/// </para>
/// <para>
/// Use <see cref="DependsOnAttribute.Optional"/> = <c>true</c> for dependencies that may or may not be present.
/// Optional dependencies are not auto-registered and won't cause validation errors if missing.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Required dependency - Module1 will be auto-registered if not present
/// [DependsOn&lt;Module1&gt;]
/// public class Module2 : Module&lt;string&gt; { }
///
/// // Optional dependency - Module1 won't be auto-registered
/// [DependsOn&lt;Module1&gt;(Optional = true)]
/// public class Module3 : Module&lt;string&gt; { }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = true)]
public class DependsOnAttribute<TModule> : DependsOnAttribute
where TModule : IModule
{
/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute{TModule}"/> class.
/// </summary>
public DependsOnAttribute() : base(typeof(TModule), skipValidation: true)
{
}
}
}
4 changes: 2 additions & 2 deletions src/ModularPipelines/Context/IModuleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ TModule GetModule<TModule>()
/// <b>Example usage:</b>
/// </para>
/// <code>
/// [DependsOn&lt;BuildModule&gt;(IgnoreIfNotRegistered = true)]
/// [DependsOn&lt;BuildModule&gt;(Optional = true)]
/// public class DeployModule : Module&lt;DeployResult&gt;
/// {
/// protected override async Task&lt;DeployResult&gt; ExecuteAsync(
Expand All @@ -170,7 +170,7 @@ TModule GetModule<TModule>()
/// </code>
/// <para>
/// <b>Important:</b> If you use this method with a module that may be registered, consider using
/// <see cref="Attributes.DependsOnAttribute.IgnoreIfNotRegistered"/> on your dependency attribute
/// <see cref="Attributes.DependsOnAttribute.Optional"/> on your dependency attribute
/// to properly handle the optional dependency in the execution graph.
/// </para>
/// </remarks>
Expand Down
8 changes: 4 additions & 4 deletions src/ModularPipelines/Engine/DependencyGraphValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ private static IEnumerable<Type> GetDependencyTypes(Type moduleType, HashSet<Typ
foreach (var attribute in moduleType.GetCustomAttributesIncludingBaseInterfaces<DependsOnAttribute>())
{
// Only include if this dependency type is actually being registered
// Also handle IgnoreIfNotRegistered - if the dependency is not registered and
// IgnoreIfNotRegistered is true, we skip it for cycle detection
// Also handle Optional - if the dependency is not registered and
// Optional is true, we skip it for cycle detection
if (availableModuleTypes.Contains(attribute.Type))
{
yield return attribute.Type;
}
else if (!attribute.IgnoreIfNotRegistered)
else if (!attribute.Optional)
{
// If the dependency is not registered and IgnoreIfNotRegistered is false,
// If the dependency is not registered and Optional is false,
// we still yield it so the runtime can fail appropriately later.
// For cycle detection, we only care about registered modules.
}
Expand Down
16 changes: 7 additions & 9 deletions src/ModularPipelines/Engine/Execution/DependencyWaiter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task WaitForDependenciesAsync(ModuleState moduleState, IModuleSched
// Get both attribute-based and programmatic dependencies
var dependencies = GetAllDependencies(moduleState);

foreach (var (dependencyType, ignoreIfNotRegistered) in dependencies)
foreach (var (dependencyType, optional) in dependencies)
{
var dependencyTask = scheduler.GetModuleCompletionTask(dependencyType);

Expand All @@ -46,15 +46,13 @@ public async Task WaitForDependenciesAsync(ModuleState moduleState, IModuleSched
depLogger.LogError(e, "Ignoring Exception due to 'AlwaysRun' set");
}
}
else if (!ignoreIfNotRegistered)
else if (!optional)
{
var message = $"Module '{moduleState.ModuleType.Name}' depends on '{dependencyType.Name}', " +
$"but '{dependencyType.Name}' has not been registered in the pipeline.\n\n" +
var message = $"Module '{moduleState.ModuleType.Name}' requires '{dependencyType.Name}', " +
$"but '{dependencyType.Name}' has not been registered and could not be auto-registered.\n\n" +
$"Suggestions:\n" +
$" 1. Add '.AddModule<{dependencyType.Name}>()' to your pipeline configuration before '.AddModule<{moduleState.ModuleType.Name}>()'\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";
$" 1. Add '.AddModule<{dependencyType.Name}>()' to your pipeline configuration\n" +
$" 2. Use '[DependsOn<{dependencyType.Name}>(Optional = true)]' if this dependency is optional";
throw new ModuleNotRegisteredException(message, null);
}
}
Expand All @@ -63,7 +61,7 @@ public async Task WaitForDependenciesAsync(ModuleState moduleState, IModuleSched
/// <summary>
/// Gets all dependencies for a module, combining attribute-based and programmatic dependencies.
/// </summary>
private static IEnumerable<(Type DependencyType, bool IgnoreIfNotRegistered)> GetAllDependencies(ModuleState moduleState)
private static IEnumerable<(Type DependencyType, bool Optional)> GetAllDependencies(ModuleState moduleState)
{
// Attribute-based dependencies
foreach (var dep in ModuleDependencyResolver.GetDependencies(moduleState.ModuleType))
Expand Down
Loading
Loading