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:
- Auto-registration: If the dependency module is not explicitly registered, ModularPipelines will automatically register it for you
- Validation: The pipeline validates that all required dependencies can be resolved before execution
- 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
}
}