I want to expose reading and parsing data from a file asynchronously into strings, through an IAsyncEnumerable<string>
, so my calling code can process the data as it comes in.
The problem is that I have to pass a disposable (here modeled by a StreamReader
) to the parser, and while it must be disposed after use the parser can't do that.
var parser = new Parser();
var wrapper = new Wrapper(parser);
// My calling code, fake reading a file and then process it:
using var stream = new MemoryStream("Multi\nLine\nString"u8.ToArray());
await foreach (var s in wrapper.ReadStringsAsync(stream))
{
Console.WriteLine(s);
}
// The "wrapper" class, to hide that I'm using a StreamReader, and dispose it after we're done:
public class Wrapper
{
private readonly Parser _parser;
public Wrapper(Parser parser) => _parser = parser;
public IAsyncEnumerable<string> ReadStringsAsync(Stream stream)
{
using var reader = new StreamReader(stream);
if (reader.EndOfStream)
{
throw new InvalidOperationException("Can't read an empty file");
}
return _parser.ReadStringsAsync(reader);
}
}
// The "parser" calling the StreamReader's ReadLineAsync()
public class Parser
{
public async IAsyncEnumerable<string> ReadStringsAsync(StreamReader reader)
{
while (true)
{
var line = await reader.ReadLineAsync();
if (line == null)
{
yield break;
}
yield return line;
}
}
}
The whole point of the Wrapper
is to hide from the caller that a StreamReader
is being used, and the whole point of the Parser
class is that it can only call ReadLineAsync()
and cannot dispose the reader.
For all intents and purposes, it's not really a StreamReader
, it's not really a direct dependency (more like reader.GetAnotherDisposable()
); this is a contrived example, the real-life scenario does require double disposing.
The wrapper code using var reader
disposes the StreamReader even before the first line can be read:
System.ObjectDisposedException: Cannot read from a closed TextReader.
How can I refactor the non-async IAsyncEnumerable<string> Wrapper.ReadStringsAsync()
method, so that:
- It can be called using
await foreach()
. - It hides the use of the
StreamReader
from the caller. - It still owns the
StreamReader
, because it needs to call methods on it before returning theIAsyncEnumerable
, and it must be disposed afterwards. - It won't be made async, because it can't
yield return
. - It only disposes the
StreamReader
after all its contents have been enumerated.
While writing this question I found two solutions:
Option 1, regarding point 4 above: Return IAsyncEnumerable from an async method
Make it async indeed, have it enumerate the results asynchronously and yield return them again:
public async IAsyncEnumerable<string> ReadStringsAsync(Stream stream)
{
using var reader = new StreamReader(stream);
if (reader.EndOfStream)
{
throw new InvalidOperationException("Can't read an empty file");
}
await foreach (var s in _parser.ReadStringsAsync(reader))
{
yield return s;
}
}
But that feels like asyncception. Doesn't this compile into an extraneous state machine and enumerator that do nothing but proxy the actual work (i.e. copy the string values straight into another async state machine) and add overhead?
Option 2, regarding point 5 above: Correct disposal in IAsyncEnumerable methods?
Return the disposable from the non-async by making it an out
variable:
public IAsyncEnumerable<string> ReadStringsDisposable(Stream stream, out IDisposable disposeMe)
{
var reader = new StreamReader(stream);
disposeMe = reader;
if (reader.EndOfStream)
{
throw new InvalidOperationException("Can't read an empty file");
}
return _parser.ReadStringsAsync(reader);
}
Then dispose it after use:
IDisposable? disposeMe = null;
try
{
await foreach (var s in wrapper.ReadStringsAsync(stream, out disposeMe))
{
Console.WriteLine(s);
}
}
finally
{
disposeMe?.Dispose();
}
But that changes the signature, which I'd rather not do, because it prevents refactoring the implementation later.
An alternative to the latter would be to create a custom IAsyncEnumerable/tor
implementation that does the disposing on disposal of the enumerator.
So how can I dispose the class without creating a "useless" state machine (making it async, calling await foreach() yield return
) and without modifying the signature into an unrefactorable one?