TaskFlow turns calls from many places into one owned FIFO lane of work. Every submission gets an awaitable result while the lane serializes execution and provides a clear lifetime boundary.
Applications often need to accept work asynchronously while ensuring that only one operation touches a resource at a time and that operations retain their original order. Building that around a semaphore or task chain leaves ordering, per-call completion, cancellation, error observation, and shutdown ownership in application code.
TaskFlow packages those concerns into a reusable execution lane. It is useful when you need to:
- expose an asynchronous API over a synchronous or non-thread-safe resource;
- preserve event order when synchronous callbacks initiate work;
- own background work within a component or dependency-injection scope;
- cancel obsolete operations when a newer request arrives;
- add timeouts, throttling, logging, annotations, or error observation without changing the work itself; or
- run ordered work on the thread pool, a dedicated thread, a caller-owned thread, or a custom scheduler.
One flow is one sequential lane. Create separate flows for work that should proceed independently.
| Feature | What it provides |
|---|---|
| FIFO execution | Accepted operations start in submission order and do not overlap within one flow. |
| Per-operation tasks | Every caller can await its own result, exception, or cancellation. |
| Owned lifetime | A flow gives queued and running work an explicit component-level shutdown boundary. |
| Composable policies | Add cancellation scopes, latest-request-wins behavior, timeouts, leading-edge throttling, operation names, interception, and error observation. |
| Execution choices | Use the thread pool, a dedicated thread, the current thread, or another TaskScheduler. |
| Application integration | Register scoped or named flows and emit structured lifecycle logs. |
| Extensibility | Build scheduler decorators, adapters, interceptors, or custom TaskFlowBase implementations. |
See the extension reference and execution models for the available policies and implementations.
using System.Threading.Tasks.Flow;
public interface IDataStore
{
void Save(Data data);
}
public sealed class SerializedStore(IDataStore inner) : IAsyncDisposable
{
private readonly TaskFlow _flow = new();
public Task SaveAsync(Data data) =>
_flow.Enqueue(() => inner.Save(data));
public ValueTask DisposeAsync() => _flow.DisposeAsync();
}Callers receive a task instead of blocking on Save. The wrapped synchronous method runs once at a time and in call order, regardless of how many callers submit work concurrently.
| Package | Purpose |
|---|---|
TaskFlow |
FIFO execution lanes, built-in execution models, and core scheduler policies. |
TaskFlow.Extensions.Time |
Compatibility package for the WithThrottle time-based policy. |
TaskFlow.Microsoft.Extensions.DependencyInjection |
Scoped, named, and customizable TaskFlow registrations. |
TaskFlow.Microsoft.Extensions.Logging |
Structured operation-lifecycle logging through Microsoft.Extensions.Logging. |
- Await returned tasks when their outcome belongs to the caller; intentionally discarded work remains bounded by the flow and can report failures inside the operation or through a decorator when needed.
- Prefer
await usingso asynchronous disposal can wait for the lane to finish. - Cancellation is cooperative, and synchronous disposal has a timeout.
- Scheduler decorators do not own the underlying flow; dispose the original
ITaskFlow.
Read Concepts and lifecycle and Semantics and pitfalls for the full behavior contract.
TaskFlow is available under the MIT License. Contributions and problem reports are welcome through GitHub issues.