$npx -y skills add wshaddix/dotnet-skills --skill dotnet-channelsUsing producer/consumer queues. Channel<T>, bounded/unbounded, backpressure, drain patterns
| 1 | # dotnet-channels |
| 2 | |
| 3 | Deep guide to `System.Threading.Channels` for high-performance, thread-safe producer/consumer communication in .NET. Covers channel creation, backpressure strategies, IAsyncEnumerable integration, and graceful shutdown patterns. |
| 4 | |
| 5 | **Out of scope:** Hosted service lifecycle and `BackgroundService` registration are owned by [skill:dotnet-background-services]. Async/await fundamentals and cancellation token propagation are owned by [skill:dotnet-csharp-async-patterns]. |
| 6 | |
| 7 | Cross-references: [skill:dotnet-background-services] for integrating channels with hosted services, [skill:dotnet-csharp-async-patterns] for async patterns used in channel consumers. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Channel<T> Fundamentals |
| 12 | |
| 13 | A `Channel<T>` is a thread-safe data structure with separate `ChannelWriter<T>` and `ChannelReader<T>` endpoints. Writers produce items, readers consume them -- the channel handles all synchronization. |
| 14 | |
| 15 | ```csharp |
| 16 | // Create a channel and separate the endpoints |
| 17 | Channel<WorkItem> channel = Channel.CreateUnbounded<WorkItem>(); |
| 18 | ChannelWriter<WorkItem> writer = channel.Writer; |
| 19 | ChannelReader<WorkItem> reader = channel.Reader; |
| 20 | ``` |
| 21 | |
| 22 | ### Bounded vs Unbounded |
| 23 | |
| 24 | | Aspect | Bounded | Unbounded | |
| 25 | |--------|---------|-----------| |
| 26 | | Creation | `Channel.CreateBounded<T>(capacity)` | `Channel.CreateUnbounded<T>()` | |
| 27 | | Back-pressure | Yes -- `FullMode` controls behavior when full | No -- grows without limit | |
| 28 | | Memory safety | Capped at `capacity` items | Can exhaust memory under load | |
| 29 | | Use when | Production workloads, untrusted producer rates | Guaranteed-low-volume, prototyping | |
| 30 | |
| 31 | ```csharp |
| 32 | // Bounded -- preferred for production |
| 33 | var bounded = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(capacity: 1000) |
| 34 | { |
| 35 | FullMode = BoundedChannelFullMode.Wait |
| 36 | }); |
| 37 | |
| 38 | // Unbounded -- use only when you control the producer rate |
| 39 | var unbounded = Channel.CreateUnbounded<WorkItem>(); |
| 40 | ``` |
| 41 | |
| 42 | --- |
| 43 | |
| 44 | ## BoundedChannelFullMode |
| 45 | |
| 46 | Controls what happens when a bounded channel is full and a producer attempts to write. |
| 47 | |
| 48 | | Mode | Behavior | Use case | |
| 49 | |------|----------|----------| |
| 50 | | `Wait` | `WriteAsync` blocks until space is available | Default. Reliable delivery with back-pressure | |
| 51 | | `DropOldest` | Drops the oldest item in the channel to make room | Telemetry, metrics -- latest data matters most | |
| 52 | | `DropNewest` | Drops the item being written (newest) | Rate limiting -- discard excess incoming work | |
| 53 | | `DropWrite` | Drops the item being written and returns `false` from `TryWrite` | Non-blocking fire-and-forget with overflow detection | |
| 54 | |
| 55 | ```csharp |
| 56 | // DropOldest -- telemetry pipeline where stale readings are expendable |
| 57 | var telemetryChannel = Channel.CreateBounded<SensorReading>(new BoundedChannelOptions(500) |
| 58 | { |
| 59 | FullMode = BoundedChannelFullMode.DropOldest |
| 60 | }); |
| 61 | |
| 62 | // DropWrite -- non-blocking enqueue with overflow awareness |
| 63 | var logChannel = Channel.CreateBounded<LogEntry>(new BoundedChannelOptions(10_000) |
| 64 | { |
| 65 | FullMode = BoundedChannelFullMode.DropWrite |
| 66 | }); |
| 67 | |
| 68 | if (!logChannel.Writer.TryWrite(entry)) |
| 69 | { |
| 70 | // Channel full -- item was dropped; track overflow metric |
| 71 | overflowCounter.Add(1); |
| 72 | } |
| 73 | ``` |
| 74 | |
| 75 | ### itemDropped Callback (.NET 7+) |
| 76 | |
| 77 | Starting in .NET 7, bounded channels with drop modes accept an `itemDropped` callback that fires whenever an item is discarded. Use this for metrics, logging, or resource cleanup on dropped items. |
| 78 | |
| 79 | ```csharp |
| 80 | var channel = Channel.CreateBounded(new BoundedChannelOptions(100) |
| 81 | { |
| 82 | FullMode = BoundedChannelFullMode.DropOldest |
| 83 | }, |
| 84 | itemDropped: (item, writer) => |
| 85 | { |
| 86 | logger.LogWarning("Dropped item due to channel overflow: {Id}", item.Id); |
| 87 | droppedItemsCounter.Add(1); |
| 88 | // Clean up disposable items if needed |
| 89 | (item as IDisposable)?.Dispose(); |
| 90 | }); |
| 91 | ``` |
| 92 | |
| 93 | The callback receives the dropped item and the `ChannelWriter<T>` (useful if you need to re-route items to a fallback channel). |
| 94 | |
| 95 | --- |
| 96 | |
| 97 | ## Producer Patterns |
| 98 | |
| 99 | ### Single Producer |
| 100 | |
| 101 | ```csharp |
| 102 | // Write with back-pressure (bounded channels) |
| 103 | await writer.WriteAsync(item, cancellationToken); |
| 104 | |
| 105 | // Non-blocking write attempt (returns false if channel is full or completed) |
| 106 | if (!writer.TryWrite(item)) |
| 107 | { |
| 108 | // Handle overflow -- log, retry, or discard |
| 109 | } |
| 110 | ``` |
| 111 | |
| 112 | ### Multiple Producers |
| 113 | |
| 114 | Multiple producers can call `WriteAsync` or `TryWrite` concurrently without external locking. The channel is internally thread-safe. |
| 115 | |
| 116 | ```csharp |
| 117 | // Multiple API endpoints enqueueing work into a shared channel |
| 118 | app.MapPost("/api/orders/{id}/process", async ( |
| 119 | string id, |
| 120 | ChannelWriter<OrderCommand> writer, |
| 121 | CancellationToken ct) => |
| 122 | { |
| 123 | await writer.WriteAsync(new OrderCommand(id, "process"), ct); |
| 124 | return Results.Accepted(); |
| 125 | }); |
| 126 | |
| 127 | app.MapPost("/api/orders/{id}/cancel", async ( |
| 128 | string id, |
| 129 | ChannelWriter<OrderCommand> writer, |
| 130 | CancellationToken ct) => |
| 131 | { |
| 132 | await writer.WriteAsync(new OrderCommand(id, "cancel"), ct); |
| 133 | return Results.Accepted(); |
| 134 | }); |
| 135 | ``` |
| 136 | |
| 137 | # |