Skip to content

Provide a version of Stream.Read that reads as much as possible #16598

Closed
@svick

Description

@svick

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 into buffer.
  • When throwOnEndOfStream == true, the return value will always be minimumBytes <= bytesRead <= buffer.Length. If the end of the stream is detected, an EndOfStreamException will be thrown.
  • When throwOnEndOfStream == false, the return value will always be bytesRead <= buffer.Length. Callers can check for end of the stream by checking bytesRead < 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

  1. We could add a convenience wrapper (ReadAll or Fill) that doesn't take int minimumBytes, and uses buffer.Length as the minimumBytes.

    1. This can be accomplished by simply passing in buffer.Length
    2. The APIs wouldn't be overloads since the names (ReadAtLeast and ReadAll) wouldn't match. I can't think of a decent common name that would work for both operations.
    3. We can always add it later, if we get enough feedback that it is necessary.
  2. 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 for ReadAtLeast. They already get passed the buffer length, if they want a hint of how much data is being requested.

    1. We can always add it later, if we get enough feedback that it is necessary.
    2. The one place I did find that could possibly override ReadAtLeast is in PipeReader.AsStream()'s implementation. Since PipeReader has a ReadAtLeastAsync API, the PipeReaderStream could override ReadAtLeastAsync and forward the call directly to PipeReader.ReadAtLeastAsync. However, there is no synchronous API on PipeReader, 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 have async version?

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.IOblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions