Firstly, I want to say that I love C#’s Channels. The interface they provide is in almost all cases entirely frictionless to my process. They are easy to understand and don’t annoy me with too much unnecessary boilerplate. That’s why a recent experience working with them was all the more frustrating to me.
The issue at hand
I have a setup where I can create an operation with an ID and at a different point in time retrieve details about that operation from a Channel with that ID. It is mainly relevant for operations where I send updates for a long-running operation via server-sent events. Upon the start of my application, I also triggered one of those long-running operations and wanted to await the completion of that operation.
Easy enough, I thought. The writer of the update completes the ChannelWriter when it finishes the operation. Following the documentation for the ChannelReader, I thought that the Completion property would be just what I wanted. It
Gets a Task that completes when no more data will ever be available to be read from this channel.
So my assumption was that once completed, the Completion task would fire - annoyingly it did not.
Liars surround me
The documentation stating that a completion is triggered once no more data will ever be available is a little ambiguous in my opinion. How I understood it was that no further data could ever be added to a Channel. Which would fit its counterpart method on the writer being Complete(). This is not the case, though, as I will reveal later.
This isn’t really my main issue, although the documentation could be better in its wording. My main issue was to then consult ChatGPT to get a second “pair of eyes” to take a look.
Not only was it not helpful, it actively misguided me to doubt different parts of my implementation that were fine all along. Initially my ID-bound Channel implementation was identified to be the culprit. After some back and forth and second-guessing myself, I hoped I had convinced ChatGPT that the problem really was with the ChannelReader itself, yet the next answer was
✅ 2. Why await reader.Completion hangs Even on an unbounded channel, .Completion only fires when: 1. The writer calls Complete(), 2. The channel finishes propagating completion, 3. No obstruction or premature teardown interferes
Your implementation breaks (2): - ClearUpdates removes the channel before the completion signal transitions. - The reader sees a channel whose completion task never finishes.
This is a known subtle trap with channels when you “clear” or “forget” them too early.
Followed by this code snippet as a fix
await foreach (var _ in reader.ReadAllAsync()) { }
Which would not change a thing if the reasons given before were actually what is wrong. So I discarded the thought and moved on to try and find the issue.
Why Reading works
In the end, reading everything from the Channel really is the solution. Not because of the reasons that ChatGPT gave, though.
As stated before, a Channel triggers its completion task when no more data will ever be available. Which is to be understood such that the Channel is empty and will never receive new data. So setting the ChannelWriter to completed is only half the equation. Much to my own detriment, I discarded the solution ChatGPT gave me for far too long, simply because the reasoning didn’t make sense, so I assumed the solution would also not be correct.
Turns out I didn’t even need ChatGPT because understanding that the Channel needed to be emptied was something that I figured out on my own. Out of spite, I then informed my unhelpful LLM what the actual reason was, and to add insult to injury, after giving the crucial information, I received a message detailing the very details of ChannelReader handling I would have liked to have gotten when I first asked. Not after I have already solved the case myself.
To cut a long story short. Don’t overuse LLMs, write precise documentation, and remember that Channels only complete when all data has been read.