The answer to this may be that it's not possible, but the question is: assume you have a C# method for consuming the lines in a TextReader
that returns IAsyncEnumerable<string>
. How do you ensure that when DisposeAsync
is called on the IAsyncEnumerator<string>
that the TextReader
is disposed of? Or is this something you need to write a custom implementation to achieve?
Asked
Active
Viewed 1,302 times
3

Julian Birch
- 2,605
- 1
- 21
- 36
-
1It's not actually the enumeration's job to dispose the `TextReader`, that's the job of whatever created the `TextReader`. Normally you'd expect that code to live in its own `Task` with its own `using`. In theory you could just pass the reader to the enumerator and *make* it responsible, but that would reduce the scenarios it would actually be useful in. Conceptually, enumerators are distinct from the things they're enumerating. – Jeroen Mostert Oct 11 '19 at 16:44
-
1Note that if you have an iterator method that *itself* creates the `TextReader`, all you need to do is make sure it does a `using var reader = new TextReader(...)`; compiler magic will transform the iterator's using blocks into something called on dispose, just as for non-async iterators. – Jeroen Mostert Oct 11 '19 at 16:49
1 Answers
6
Just use a try-finally block inside the async iterator, or more simply a using
block, and the TextReader
will be disposed as soon as the caller completes the enumeration. It doesn't matter if the enumeration will complete normally, or prematurely because of an exception or a break
.
static async Task Main(string[] args)
{
await foreach (var line in GetLines())
{
Console.WriteLine(line);
}
}
private static async IAsyncEnumerable<string> GetLines()
{
var reader = new StringReader("Line1\nLine2\nLine3");
try
{
while (true)
{
var line = await reader.ReadLineAsync();
if (line == null) break;
yield return line;
}
}
finally
{
reader.Dispose();
Console.WriteLine("Disposed");
}
}
Output:
Line1
Line2
Line3
Disposed

Theodor Zoulias
- 34,835
- 7
- 69
- 104
-
1If the foreach is an infinite loop that never ends, the enumerator will not be disposed, and the finally block will not be called. This is the only case I can think of. – Theodor Zoulias Oct 18 '19 at 09:31
-
Another case is if a buggy or malicious caller creates an enumerator (by calling the [`GetAsyncEnumerator`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1.getasyncenumerator) method), and then keep a reference to it in a static field to prevent it from being garbage collected. This possibility is also open for normal (not async) enumerables/enumerators. – Theodor Zoulias Oct 18 '19 at 09:37
-
The real case I'm thinking of is if someone just breaks out of the loop because they've got the data they need. (I'm less concerned with bugs/malice.) – Julian Birch Oct 18 '19 at 10:38
-
3This case is covered. Breaking the `foreach` loop causes the immediate disposal of the enumerator used in the loop, causing the invocation of the finally block inside the iterator. – Theodor Zoulias Oct 18 '19 at 10:41