The biggest release yet. V3 modernizes the entire API to match ASP.NET Core patterns you already know, adds powerful new features, and makes your pipelines cleaner and more maintainable.
No more callbacks. Direct property access, just like ASP.NET Core minimal APIs.
// Before (V2)
await PipelineHostBuilder.Create()
.ConfigureAppConfiguration((context, builder) => { ... })
.ConfigureServices((context, collection) => { ... })
.ExecutePipelineAsync();
// After (V3)
var builder = Pipeline.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json");
builder.Services.AddModule<BuildModule>();
await builder.Build().RunAsync();If you've used ASP.NET Core, this feels instantly familiar.
Configure module behavior with a clean, fluent API instead of scattered property overrides.
// Before (V2) - properties scattered across the class
protected internal override TimeSpan Timeout => TimeSpan.FromMinutes(5);
protected override AsyncRetryPolicy<string?> RetryPolicy => ...;
protected internal override Task<SkipDecision> ShouldSkip(...) => ...;
// After (V3) - everything in one place
protected override ModuleConfiguration Configure() => ModuleConfiguration.Create()
.WithTimeout(TimeSpan.FromMinutes(5))
.WithRetryCount(3)
.WithSkipWhen(ctx => ctx.Git().Information.BranchName != "main"
? SkipDecision.Skip("Only runs on main")
: SkipDecision.DoNotSkip)
.Build();Module results are now discriminated unions. Pattern matching gives you compile-time safety.
var result = await context.GetModule<BuildModule>();
return result switch
{
ModuleResult<BuildOutput>.Success { Value: var output } => Deploy(output),
ModuleResult.Skipped => null,
ModuleResult.Failure { Exception: var ex } => throw ex,
_ => null
};Or use the simpler helpers for quick migrations:
if (result.IsSuccess)
{
var value = result.ValueOrDefault;
}New Module and SyncModule base classes for modules that don't return data.
// Async module - no return value needed
public class DeployModule : Module
{
protected override async Task ExecuteModuleAsync(
IModuleContext context, CancellationToken cancellationToken)
{
await context.Command.ExecuteCommandLineTool(...);
}
}
// Sync module - no return value needed
public class LoggingModule : SyncModule
{
protected override void ExecuteModule(
IModuleContext context, CancellationToken cancellationToken)
{
context.Logger.LogInformation("Done!");
}
}Internally these use the None struct, which represents "nothing" and is semantically equivalent to null.
Declare dependencies programmatically based on runtime conditions.
protected override void DeclareDependencies(IDependencyDeclaration deps)
{
deps.DependsOn<RequiredModule>();
deps.DependsOnOptional<OptionalModule>();
deps.DependsOnIf<ProductionModule>(Environment.IsProduction);
}// Depend on all modules in a category
[DependsOnModulesInCategory("Build")]
public class TestModule : Module<TestResults> { }
// Depend on all modules with a tag
[DependsOnModulesWithTag("database")]
public class MigrationModule : Module<bool> { }[RunOnLinux]
public class LinuxModule : Module<string> { }
[RunOnWindowsOnly] // Skips on other platforms
public class WindowsOnlyModule : Module<string> { }
[SkipIf(typeof(IsNotMainBranchCondition))]
public class MainBranchModule : Module<string> { }
[RunIfAll(typeof(IsCI), typeof(IsMainBranch))]
public class CIMainModule : Module<string> { }Organize modules for easier management.
[ModuleTag("critical")]
[ModuleTag("deployment")]
[ModuleCategory("Infrastructure")]
public class DeployModule : Module<DeployResult> { }Catch configuration errors before execution.
var validation = await builder.ValidateAsync();
if (validation.HasErrors)
{
foreach (var error in validation.Errors)
{
Console.WriteLine($"[{error.Category}] {error.Message}");
}
}Create reusable pipeline extensions.
public class MyPlugin : IModularPipelinesPlugin
{
public string Name => "MyPlugin";
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMyService, MyService>();
}
public void ConfigurePipeline(PipelineBuilder builder)
{
builder.Services.AddModule<PluginModule>();
}
}
[assembly: ModularPipelinesPlugin(typeof(MyPlugin))]New overridable methods for fine-grained control.
protected override Task OnBeforeExecuteAsync(IModuleContext context, CancellationToken ct) { }
protected override Task OnAfterExecuteAsync(IModuleContext context, ModuleResult<T> result, CancellationToken ct) { }
protected override Task OnSkippedAsync(IModuleContext context, SkipDecision decision, CancellationToken ct) { }
protected override Task OnFailedAsync(IModuleContext context, Exception ex, CancellationToken ct) { }| V2 | V3 |
|---|---|
PipelineHostBuilder.Create() |
Pipeline.CreateBuilder(args) |
.ConfigureAppConfiguration(callback) |
builder.Configuration |
.ConfigureServices(callback) |
builder.Services |
.ConfigurePipelineOptions(callback) |
builder.Options |
.AddModule<T>() on builder |
builder.Services.AddModule<T>() |
.ExecutePipelineAsync() |
.Build().RunAsync() |
| V2 | V3 |
|---|---|
IPipelineContext in ExecuteAsync |
IModuleContext |
GetModule<T>() on module |
context.GetModule<T>() |
Timeout property override |
Configure().WithTimeout() |
RetryPolicy property override |
Configure().WithRetryCount() |
ShouldSkip() method |
Configure().WithSkipWhen() |
ShouldIgnoreFailures() method |
Configure().WithIgnoreFailures() |
ModuleRunType.AlwaysRun |
Configure().WithAlwaysRun() |
OnBeforeExecute() |
Configure().WithBeforeExecute() or OnBeforeExecuteAsync() |
OnAfterExecute() |
Configure().WithAfterExecute() or OnAfterExecuteAsync() |
| V2 | V3 |
|---|---|
result.Value |
result.ValueOrDefault or pattern match |
result.Exception |
result.ExceptionOrDefault or pattern match |
result.ModuleResultType == ModuleResultType.Success |
result.IsSuccess or pattern match |
Execution-related properties moved from tool options to a separate CommandExecutionOptions parameter:
| V2 (on tool options) | V3 (on CommandExecutionOptions) |
|---|---|
WorkingDirectory |
WorkingDirectory |
EnvironmentVariables |
EnvironmentVariables |
ThrowOnNonZeroExitCode |
ThrowOnNonZeroExitCode |
// V3: Pass execution options as second parameter
await context.DotNet().Build(
new DotNetBuildOptions { Configuration = "Release" },
new CommandExecutionOptions { WorkingDirectory = "/app" });PipelineHostBuilder- UsePipeline.CreateBuilder()ModuleBase/ModuleBase<T>- UseModule<T>
The ExecutePipelineAsync() extension still exists:
var builder = Pipeline.CreateBuilder(args);
builder.Services.AddModule<MyModule>();
await builder.ExecutePipelineAsync(); // Still worksAnd ValueOrDefault provides backwards-compatible result access:
var result = await context.GetModule<BuildModule>();
var value = result.ValueOrDefault; // Similar to old result.ValueFor the cleanest code, adopt the new patterns:
- Use
Pipeline.CreateBuilder(args)with direct property access - Move module configuration to
Configure()builder - Change
IPipelineContexttoIModuleContext - Move
GetModule<T>()calls to context - Use pattern matching for result handling
See the Migration Guide for detailed examples.
- Update the NuGet package:
dotnet add package ModularPipelines --version 3.0.0 - Fix compile errors using the migration tables above
- (Optional) Refactor to use new fluent APIs
- (Optional) Adopt new features like tags, categories, and conditional attributes
- Full Migration Guide
- GitHub Issues - Use the
migrationlabel - Examples