Lightweight task runner with dependency graph resolution and parallel execution.
dotnet add package Philiprehberger.TaskDependencyRunnerLightweight task runner with dependency graph resolution and parallel execution.
dotnet add package Philiprehberger.TaskDependencyRunner
using Philiprehberger.TaskDependencyRunner;
var graph = new TaskGraph()
.Add("clean", () => Console.WriteLine("Cleaning..."))
.Add("restore", () => Console.WriteLine("Restoring packages..."), "clean")
.Add("build", () => Console.WriteLine("Building..."), "restore")
.Add("test", () => Console.WriteLine("Running tests..."), "build")
.Add("pack", () => Console.WriteLine("Packing..."), "build");
await graph.RunAsync();
Tasks can produce results that downstream tasks consume via ITaskContext:
using Philiprehberger.TaskDependencyRunner;
var graph = new TaskGraph()
.Add<int>("fetch-count", _ => Task.FromResult(42))
.Add<string>("format", ctx =>
{
var count = ctx.GetResult<int>("fetch-count");
return Task.FromResult($"Total: {count}");
}, "fetch-count")
.Add("print", (ctx) =>
{
Console.WriteLine(ctx.GetResult<string>("format"));
return Task.CompletedTask;
}, "format");
await graph.RunAsync();
// Access results after execution
var formatted = (string)graph.TaskResults["format"]!;
Set a timeout on individual tasks. If the timeout elapses, a TaskTimeoutException is thrown:
var graph = new TaskGraph()
.Add("fast", () => Console.WriteLine("Done"))
.Add("slow", async () => await Task.Delay(10_000), timeout: TimeSpan.FromSeconds(2));
try
{
await graph.RunAsync();
}
catch (TaskTimeoutException ex)
{
Console.WriteLine($"{ex.TaskName} timed out");
}
Track execution progress with the IProgressReporter interface:
var graph = new TaskGraph
{
ProgressReporter = new ConsoleProgressReporter()
};
graph
.Add("clean", () => { })
.Add("build", () => { }, "clean")
.Add("test", () => { }, "build");
await graph.RunAsync();
// Output:
// [START] clean
// [DONE] clean (1ms)
// [START] build
// [DONE] build (0ms)
// [START] test
// [DONE] test (0ms)
Validate the graph and inspect the execution plan without running any tasks:
var graph = new TaskGraph()
.Add("a", () => { })
.Add("b", () => { })
.Add("c", () => { }, "a", "b")
.Add("d", () => { }, "c");
var plan = graph.DryRun();
foreach (var (batch, i) in plan.Batches.Select((b, i) => (b, i)))
Console.WriteLine($"Batch {i}: {string.Join(", ", batch)}");
// Batch 0: a, b
// Batch 1: c
// Batch 2: d
Limit how many tasks run in parallel:
var graph = new TaskGraph { MaxConcurrency = 2 };
graph
.Add("a", async () => await Task.Delay(100))
.Add("b", async () => await Task.Delay(100))
.Add("c", async () => await Task.Delay(100));
await graph.RunAsync();
// Circular dependency
var bad = new TaskGraph()
.Add("a", () => { }, "b")
.Add("b", () => { }, "a");
// Throws CircularDependencyException
// Missing dependency
var missing = new TaskGraph()
.Add("a", () => { }, "nonexistent");
// Throws MissingDependencyException
TaskGraph| Member | Description |
|---|---|
Add(name, Action, params string[]) | Register a synchronous task |
Add(name, Func<Task>, params string[]) | Register an async task |
Add(name, Func<ITaskContext, Task>, params string[]) | Register an async task with context access |
Add<TResult>(name, Func<ITaskContext, Task<TResult>>, params string[]) | Register a typed result task |
Add(name, action, TimeSpan?, params string[]) | Register a task with timeout (sync or async) |
GetExecutionOrder() | Return names in topological order |
RunAsync(CancellationToken) | Execute all tasks; independent tasks run in parallel |
DryRun() | Validate graph and return ExecutionPlan without executing |
MaxConcurrency | Max parallel tasks (0 = unlimited, default) |
OnTaskCompleted | Action<string, int, int>? callback (taskName, completedCount, totalCount) |
ProgressReporter | IProgressReporter? for detailed start/complete/fail events |
TaskResults | IReadOnlyDictionary<string, object?> of typed task results |
ITaskContext| Method | Description |
|---|---|
GetResult<T>(taskName) | Retrieve a dependency's typed result |
HasResult(taskName) | Check whether a result exists for the given task |
IProgressReporter| Method | Description |
|---|---|
OnTaskStarted(name) | Called when a task begins execution |
OnTaskCompleted(name, elapsed) | Called when a task completes successfully |
OnTaskFailed(name, exception) | Called when a task fails |
ExecutionPlan| Property | Type | Description |
|---|---|---|
Batches | IReadOnlyList<IReadOnlyList<string>> | Ordered batches of parallelizable tasks |
Order | IReadOnlyList<string> | Flat topological order |
TaskCount | int | Total number of tasks |
| Type | Description |
|---|---|
CircularDependencyException | The graph contains a cycle |
MissingDependencyException | A task depends on an unregistered name |
TaskTimeoutException | A task exceeded its configured timeout (TaskName property) |
dotnet build src/Philiprehberger.TaskDependencyRunner.csproj --configuration Release
If you find this project useful: