CancellableTask

Cooperative cancellation wrapper for Func<Task>. Tracks the active task per async flow using AsyncLocal<CancellableTask> and hooks into the Yield system's OnBeforeYield/OnAfterYield events to check for cancellation at every yield point. Provides fluent callback registration for begin, complete, cancel, and end lifecycle events.

Definition

Namespace: Paragon.Core.Async Assembly: Paragon.dll

public class CancellableTask

Remarks

CancellableTask is the mechanism that makes async methods cancellable without manual CancellationToken passing. It works through two key mechanisms:

  1. AsyncLocal<CancellableTask> — When InvokeAsync() runs, it sets itself as the active task in AsyncLocal storage. Because AsyncLocal flows through async/await continuations, any code downstream can find the active task.

  2. Yield hooks — A static constructor subscribes to Yield.OnBeforeYield and Yield.OnAfterYield. At every yield point (e.g., await Yield.WaitForUpdate()), ThrowIfCancellationRequested() checks whether the active task's CancellationTokenSource has been cancelled. If so, it throws OperationCanceledException.

This means any async method using Yield methods is automatically cancellable without any explicit token handling — simply call Cancel() on the CancellableTask, and the next Yield call will throw.

Lifecycle

InvokeAsync() called
  └─ onBegin callback
  └─ Set as AsyncLocal active task
  └─ taskFunction.Invoke()
       ├─ Success path:
       │    └─ onComplete callback
       │    └─ onEnd callback
       └─ Cancel path (OperationCanceledException):
            └─ onCanceled callback
            └─ onEnd callback

The Action base class uses CancellableTask internally to make all actions cancellable.

Quick Lookup

Goal
How

Create a cancellable task

new CancellableTask(async () => { ... })

Run (fire-and-forget)

task.Invoke()

Run (awaitable)

await task.InvokeAsync()

Cancel

task.Cancel()

Check if running

task.IsRunning

Register begin callback

task.OnBegin(() => { ... })

Register complete callback

task.OnComplete(() => { ... })

Register cancel callback

task.OnCanceled(() => { ... })

Register always-run callback

task.OnEnd(() => { ... })

Properties

IsRunning

Returns true while the task function is executing.

Methods

Invoke

Starts execution without returning a Task. Fire-and-forget wrapper around InvokeAsync().

InvokeAsync

Starts execution and returns an awaitable Task. Sets this task as the AsyncLocal active task, invokes the task function, and routes to the appropriate callback based on the outcome.

circle-exclamation

Cancel

Requests cancellation by calling Cancel() on the internal CancellationTokenSource. The actual cancellation occurs at the next yield point (i.e., when Yield.WaitForUpdate() or similar is awaited).

circle-info

Cancellation is cooperative — it does not interrupt the task immediately. The task continues until it hits a Yield method, at which point ThrowIfCancellationRequested() fires.

OnBegin

Registers a callback invoked when execution starts (before the task function). Returns this for fluent chaining.

OnComplete

Registers a callback invoked when the task function completes successfully (no cancellation). Returns this for fluent chaining.

OnCanceled

Registers a callback invoked when the task is cancelled (catches OperationCanceledException). Returns this for fluent chaining.

OnEnd

Registers a callback invoked after both success and cancellation (runs in the finally block). Returns this for fluent chaining.

Implementation Requirements

When using CancellableTask, you MUST:

  1. Use Yield methods (WaitForUpdate(), WaitForSeconds(), etc.) as yield points inside the task function — these are where cancellation is checked

  2. Not call InvokeAsync() while IsRunning is true (assertion fires)

You SHOULD:

  • Register OnCanceled to handle cleanup when cancellation occurs

  • Register OnEnd for cleanup that must happen regardless of outcome

  • Use the fluent API for clean callback registration

You SHOULD NOT:

  • Use Task.Delay() or await Task.Yield() inside the task function — these bypass the cancellation system

  • Call Cancel() before Invoke() / InvokeAsync() — the CancellationTokenSource does not exist until execution starts

  • Rely on immediate cancellation — there is always a delay until the next yield point

Common Pitfalls

circle-exclamation
circle-exclamation
circle-exclamation
circle-exclamation

Examples

Basic Cancellable Task

Fluent Lifecycle Registration

Integration with Action System

See Also

Last updated