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 WithThrottle and TimeProvider-based WithTimeout. |
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.
- Choosing TaskFlow
- Getting started
- Recipes
- Extensions
- Dependency injection
- Polly retry and timeout pipelines
- Troubleshooting
TaskFlow is available under the MIT License. Contributions and problem reports are welcome through GitHub issues.