Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StreamWriter.DisposeAsync disposes the underlying Stream synchronously #88246

Open
Neme12 opened this issue Jun 30, 2023 · 8 comments
Open

StreamWriter.DisposeAsync disposes the underlying Stream synchronously #88246

Neme12 opened this issue Jun 30, 2023 · 8 comments
Assignees
Milestone

Comments

@Neme12
Copy link

Neme12 commented Jun 30, 2023

Description

I discovered issue this while filing #88244 and looking at the source code. By default, StreamWriter.Dispose also disposes/closes the underlying Stream. DisposeAsync should close the underlying Stream asynchronously, but currently closes it synchronously.

See

private async ValueTask DisposeAsyncCore()
{
// Same logic as in Dispose(), but with async flushing.
Debug.Assert(GetType() == typeof(StreamWriter));
try
{
if (!_disposed)
{
await FlushAsync().ConfigureAwait(false);
}
}
finally
{
CloseStreamFromDispose(disposing: true);
}
GC.SuppressFinalize(this);
}

and
private void CloseStreamFromDispose(bool disposing)
{
// Dispose of our resources if this StreamWriter is closable.
if (_closable && !_disposed)
{
try
{
// Attempt to close the stream even if there was an IO error from Flushing.
// Note that Stream.Close() can potentially throw here (may or may not be
// due to the same Flush error). In this case, we still need to ensure
// cleaning up internal resources, hence the finally block.
if (disposing)
{
_stream.Close();
}
}
finally
{
_disposed = true;
_charLen = 0;
base.Dispose(disposing);
}
}
}

Reproduction Steps

Expected behavior

Actual behavior

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jun 30, 2023
@ghost
Copy link

ghost commented Jun 30, 2023

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

I discovered issue this while filing #88244. By default, StreamWriter.Dispose also disposes the underlying Stream. DisposeAsync should dispose the underlying Stream asynchronously, but currently disposes it synchronously.

See

private async ValueTask DisposeAsyncCore()
{
// Same logic as in Dispose(), but with async flushing.
Debug.Assert(GetType() == typeof(StreamWriter));
try
{
if (!_disposed)
{
await FlushAsync().ConfigureAwait(false);
}
}
finally
{
CloseStreamFromDispose(disposing: true);
}
GC.SuppressFinalize(this);
}

and
private void CloseStreamFromDispose(bool disposing)
{
// Dispose of our resources if this StreamWriter is closable.
if (_closable && !_disposed)
{
try
{
// Attempt to close the stream even if there was an IO error from Flushing.
// Note that Stream.Close() can potentially throw here (may or may not be
// due to the same Flush error). In this case, we still need to ensure
// cleaning up internal resources, hence the finally block.
if (disposing)
{
_stream.Close();
}
}
finally
{
_disposed = true;
_charLen = 0;
base.Dispose(disposing);
}
}
}

Reproduction Steps

Expected behavior

Actual behavior

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

Author: Neme12
Assignees: -
Labels:

area-System.IO

Milestone: -

@Neme12
Copy link
Author

Neme12 commented Jul 3, 2023

I also think it should call stream.Dispose() instead of stream.Close(). If anyone creates their own stream and implements these methods differently, I think the expectation is that when you call streamReader.Dispose(), it would also call Dispose() on the stream. But that would be a breaking change of course (although fixing this bug about calling the sync vs async Close method is technically a breaking change too).

@dersia
Copy link

dersia commented Jul 22, 2023

I will take care of this as part of #88244

@jozkee
Copy link
Member

jozkee commented Jul 24, 2023

I also think it should call stream.Dispose() instead of stream.Close()

I think for compatiblity reasons we should continue calling Close().

@jozkee jozkee added this to the 8.0.0 milestone Jul 24, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jul 24, 2023
@dersia
Copy link

dersia commented Jul 24, 2023

I also think it should call stream.Dispose() instead of stream.Close()

I think for compatiblity reasons we should continue calling Close().

yeah, stream.dispose() does not call Close on the stream, so that would be a behavioral change. I found some other issues on StreamWriter.Dispose(). Please see #88244 (comment)

@Neme12
Copy link
Author

Neme12 commented Jul 24, 2023

yeah, stream.dispose() does not call Close on the stream, so that would be a behavioral change

Right. But fixing this issue would be a behavioral change too, so if it's OK to make that behavioral change, then why shouldn't it be OK to make this other small behavioral change too.

@jozkee
Copy link
Member

jozkee commented Jul 24, 2023

@Neme12 because changing DisposeAsync is not as impactful as changing Dispose which has been around for longer.

@NinoFloris
Copy link
Contributor

NinoFloris commented Nov 27, 2023

We have an example in Npgsql https://github.com/npgsql/Npgsql/blob/main/src/Npgsql/NpgsqlRawCopyStream.cs#L353-L376 of a stream that should ideally take advantage of async if it's there.

NpgsqlRawCopyStream is the base stream used in our StreamReader/Writer derived types (same file, see NpgsqlCopyTextReader/Writer)

It seems its made very hard to have StreamWriter call DisposeAsync on the underlying stream, many methods do GetType() == typeof(StreamWriter) checks and CloseStreamDuringDispose etc are all private, the * truly * async FlushAsync implementation is also private.

I would welcome any fixes or workarounds to get StreamReader/Writer to respect DisposeAsync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants