Skip to main content

Execution and Dependencies

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.

These can chain together as appropriate. And it'll detect if two modules depend on each other.

[DependsOn<Module1>] // or [DependsOn(typeof(Module1))] for older language versions
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
  3. Cascade skipping: If a required dependency is skipped by a run condition or category filter, the dependent module is skipped too
// 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:

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:

// 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
  • A module should still run when a dependency is excluded by a category filter or run condition
  • You want conditional behavior based on what modules are registered

Category Filters and Skipped Dependencies

When a category filter skips a required dependency, ModularPipelines cascade-skips the dependent module and any modules that require it. Use an optional dependency when the dependent module should still execute:

[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)
{
// Optional dependencies are not auto-registered
return "Running tests without compile";
}

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

If the dependency above were required ([DependsOn<CompileModule>]), TestModule would be skipped whenever the category filter skipped CompileModule.

Accessing Dependency Results

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

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

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

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:

public class Module2 : Module<string>
{
protected override void DeclareDependencies(IDependencyDeclaration deps)
{
deps.DependsOn<Module1>(); // Required
deps.DependsOnOptional<Module3>(); // Optional
deps.DependsOnIf<Module4>(someCondition); // Conditional
}
}