Description
Background and motivation
One of the most common mistakes when using Stream.Read()
is that the programmer doesn't realize that Read()
may return less data than what is available in the Stream
and less data than the buffer being passed in. And even for programmers who are aware of this, having to write the same loop every single time they want to read from a Stream
is annoying.
With the .NET 6 breaking change: Partial and zero-byte reads in DeflateStream, GZipStream, and CryptoStream, it has become apparent that a Stream.Read
API that ensures you get at least n
bytes read is valuable.
API Proposal
namespace System.IO
{
public class Stream
{
+ public int ReadAtLeast(Span<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true);
+ public ValueTask<int> ReadAtLeastAsync(Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true, CancellationToken cancellationToken = default);
}
}
ReadAtLeast
will return the number of bytes read intobuffer
.- When
throwOnEndOfStream == true
, the return value will always beminimumBytes <= bytesRead <= buffer.Length
. If the end of the stream is detected, anEndOfStreamException
will be thrown. - When
throwOnEndOfStream == false
, the return value will always bebytesRead <= buffer.Length
. Callers can check for end of the stream by checkingbytesRead < minimumBytes
.
API Usage
Example 1
Stream stream = ...;
Span<byte> buffer = ...;
stream.ReadAtLeast(buffer, buffer.Length);
// Do something with the bytes in `buffer`.
Example 2
Stream stream = ...;
Span<byte> buffer = ...;
// make sure we have at least 4 bytes
const int bytesToRead = 4;
int read = stream.ReadAtLeast(buffer, bytesToRead, throwOnEndOfStream: false);
if (read < bytesToRead)
{
// Handle an early end of the stream. E.g. throw your own exception, set a flag, etc.
}
else
{
// Do something with the bytes in `buffer`
// `read` may be more than 4
}
Alternative Designs
-
We could add a convenience wrapper (
ReadAll
orFill
) that doesn't takeint minimumBytes
, and usesbuffer.Length
as theminimumBytes
.- This can be accomplished by simply passing in
buffer.Length
- The APIs wouldn't be overloads since the names (
ReadAtLeast
andReadAll
) wouldn't match. I can't think of a decent common name that would work for both operations. - We can always add it later, if we get enough feedback that it is necessary.
- This can be accomplished by simply passing in
-
There is a question of whether these methods should be
virtual
or not. From scanning the Stream implementations in dotnet/runtime, I don't see anything special a Stream could do forReadAtLeast
. They already get passed the buffer length, if they want a hint of how much data is being requested.- We can always add it later, if we get enough feedback that it is necessary.
- The one place I did find that could possibly override
ReadAtLeast
is inPipeReader.AsStream()
's implementation. SincePipeReader
has aReadAtLeastAsync
API, thePipeReaderStream
could overrideReadAtLeastAsync
and forward the call directly toPipeReader.ReadAtLeastAsync
. However, there is no synchronous API onPipeReader
, so it would just be implemented for the async API.
Original Proposal
I believe one of the most common mistakes when using Stream.Read()
is that the programmer doesn't realize that Read()
may return less data than what is available in the Stream
. And even for programmers who are aware of this, having to write the same loop every single time they want to read from a Stream
is annoying.
So, my proposal is to add the following methods to Stream
(mirroring the existing Read
methods):
public int ReadAll(byte[] buffer, int offset, int count);
public Task<int> ReadAllAsync(byte[] buffer, int offset, int count);
public Task<int> ReadAllAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken);
Each ReadAll
method would call the corresponding Read
method in a loop, until the buffer
was filled or until Read
returned 0.
Questions:
- Is there a better name than
ReadAll
? - These methods would work as well if they were extension methods. Should they be?
- Very similar functionality is already exposed through
BinaryReader.ReadBytes()
. Why doesn't it haveasync
version?