Skip to main content
Version: Next

Sharing data across modules

Modules have been designed with data and sharing at its core.

When a module returns data in its ExecuteAsync method, that data is available to be seen by other modules.

Call await context.GetModule<TModule>() from within your module to access another module's result.

[DependsOn<BuildModule>]
public class DeployModule : Module<DeployResult>
{
protected override async Task<DeployResult> ExecuteAsync(
IModuleContext context, CancellationToken cancellationToken)
{
// Get the build module's result
var buildResult = await context.GetModule<BuildModule>();

// Access the required dependency value. This throws with module context
// if the module failed, was skipped, or returned null.
var artifact = buildResult.Value.ArtifactPath;

return await Deploy(artifact);
}
}

Handling Different Outcomes

Module results are a discriminated union with three possible states: Success, Failure, or Skipped. Use pattern matching to handle each case:

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

// Pattern matching (recommended)
return result switch
{
ModuleResult<MyResult>.Success { Value: var value }
=> await ProcessValue(value),
ModuleResult<MyResult>.Failure { Exception: var ex }
=> HandleFailure(ex),
ModuleResult<MyResult>.Skipped { Decision: var skip }
=> HandleSkipped(skip.Reason),
_ => null
};

Using Match Helper

For exhaustive handling, use the Match method:

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

return result.Match(
onSuccess: value => Process(value),
onFailure: ex => HandleError(ex),
onSkipped: skip => HandleSkip(skip)
);

Accessing Required Values

Use Value when the dependency must have produced a non-null value. It returns T without a null-forgiveness operator. If the module failed, was skipped, or returned null, it throws an InvalidOperationException that identifies the module and outcome:

var result = await context.GetModule<MyModule>();
var value = result.Value;

Safe Accessors

When an absent value is expected, inspect the union through its non-throwing accessors:

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

if (result is ModuleResult<MyResult>.Success success)
{
var value = success.Value;
// Process value
}

if (result.ExceptionOrDefault is { } exception)
{
// Handle error
}

if (result.SkipDecisionOrDefault is { } skipDecision)
{
// Handle skip
}

Important: Declare Dependencies

Always declare dependencies using [DependsOn<T>] to ensure the dependent module has completed before you call GetModule:

[DependsOn<BuildModule>] // Ensures BuildModule completes first
[DependsOn<TestModule>] // Ensures TestModule completes first
public class DeployModule : Module<DeployResult>
{
protected override async Task<DeployResult> ExecuteAsync(
IModuleContext context, CancellationToken cancellationToken)
{
// Safe to call - dependencies are guaranteed to be complete
var build = await context.GetModule<BuildModule>();
var tests = await context.GetModule<TestModule>();

// ...
}
}