Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to use DisposeAsync on C# streams

I'm writing a method which asynchronously writes separate lines of text to a file. If it's cancelled it deletes the created file and jumps out of the loop.

This is the simplified code which works fine... And I marked 2 points which I'm not sure how they are being handled. I want the code to not block the thread in any case.

public async Task<IErrorResult> WriteToFileAsync(string filePath,
                                                 CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    using var stream = new FileStream(filePath, FileMode.Create);
    using var writer = new StreamWriter(stream, Encoding.UTF8);

    foreach (var line in Lines)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            //
            // [1] close, delete and throw if cancelled
            //
            writer.Close();
            stream.Close();
            if (File.Exists(filePath))
                File.Delete(filePath);
            throw new OperationCanceledException();
        }

        // write to the stream
        await writer.WriteLineAsync(line.ToString());
    }

    //
    // [2] flush and let them dispose
    //
    await writer.FlushAsync();
    await stream.FlushAsync();
    // await stream.DisposeAsync(); ??????
    return null;
}

1

I'm calling Close() on FileStream and StreamWriter and I think it will run synchronously and blocks the thread. How can I improve this? I don't want to wait for it to flush the buffer into the file and then delete the file.

2

I suppose the Dispose method will be called and not DisposeAsync at the end of the using scope. (is this assumption correct?).

So Dispose blocks the thread and in order to prevent that I'm flushing first with FlushAsync so that Dispose would perform less things. (to what extent is this true?)

I could also remove using and instead I could write DisposeAsync manually in these two places. But it will decrease readability.

If I open the FileStream with useAsync = true would it automatically call DisposeAsync when using block ends?


Any explanation or a variation of the above code which performs better is appreciated.

like image 653
Bizhan Avatar asked Nov 04 '19 15:11

Bizhan


People also ask

Why DisposeAsync?

DisposeAsync() method when you need to perform resource cleanup, just as you would when implementing a Dispose method. One of the key differences, however, is that this implementation allows for asynchronous cleanup operations. The DisposeAsync() returns a ValueTask that represents the asynchronous disposal operation.

When dispose method is called in C#?

When the close brace is reached, the Dispose( ) method will be called on the object automatically, as illustrated in Example 4-6. In the first part of this example, the Font object is created within the using statement. When the using statement ends, Dispose( ) is called on the Font object.


1 Answers

As you have it, the using statement will call Dispose(), not DisposeAsync().

C# 8 brought a new await using syntax, but for some reason it's not mentioned in the What's new in C# 8.0 article.

But it's mentioned elsewhere.

await using var stream = new FileStream(filePath, FileMode.Create);
await using var writer = new StreamWriter(stream, Encoding.UTF8);

But also note that this will only work if:

  • You're using .NET Core 3.0+ since that's when IAsyncDisposable was introduced, or
  • Install the Microsoft.Bcl.AsyncInterfaces NuGet package. Although this only adds the interfaces and doesn't include the versions of the Stream types (FileStream, StreamWriter, etc.) that use it.

Even in the Announcing .NET Core 3.0 article, IAsyncDisposable is only mentioned in passing and never expanded on.

On another note, you don't need to do this (I see why now):

writer.Close();
stream.Close();

Since the documentation for Close says:

This method calls Dispose, specifying true to release all resources. You do not have to specifically call the Close method. Instead, ensure that every Stream object is properly disposed.

Since you're using using, Dispose() (or DisposeAsync()) will be called automatically and Close won't do anything that's not already happening.

So if you do need to specifically close the file, but want to do it asynchronously, just call DisposeAsync() instead. It does the same thing.

await writer.DisposeAsync();

public async Task<IErrorResult> WriteToFileAsync(string filePath,
                                                 CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    await using var stream = new FileStream(filePath, FileMode.Create);
    await using var writer = new StreamWriter(stream, Encoding.UTF8);

    foreach (var line in Lines)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            // not possible to discard, FlushAsync is covered in DisposeAsync
            await writer.DisposeAsync(); // use DisposeAsync instead of Close to not block

            if (File.Exists(filePath))
                File.Delete(filePath);
            throw new OperationCanceledException();
        }

        // write to the stream
        await writer.WriteLineAsync(line.ToString());
    }

    // FlushAsync is covered in DisposeAsync
    return null;
}
like image 102
Gabriel Luci Avatar answered Oct 09 '22 21:10

Gabriel Luci