This is a demonstration about how to read from a Stream in one single call, while still reading a guaranteed number of bytes. The example tries to provide developers with a quick solution for exact length Stream reads, using extension methods.
Introduction
Standard implementations of Stream
don't always read the exact number of bytes we request when calling its read methods. Although this is the expected behavior, there are times when we need to read a guaranteed number of bytes and take some action when such is not possible. This article demonstrates simple ways to read a guaranteed number of bytes from a Stream
.
Background
Stream
objects provide methods like Read
and ReadAsync
that accept a byte array, a starting array index and the number of bytes to read as their parameters. These methods are not guaranteed to read the exact number of bytes we request. Instead, they return an integer representing the number of bytes read, which can even be 0 if there is no more data to read on the Stream
. This is the way these methods are designed to behave, but it may also be overlooked by many developers, which can result in unexpected application behavior.
Usually, Stream
implementations would read the number of bytes you request when calling its Read
methods, but there are circumstances where reading the requested number of bytes may not be possible (for instance, on network related streams or corrupted files). If you're writing a method that accepts Stream
objects, and you don't know how the provided Stream
derived object is implemented, you may wish to consider every situation and always assume reads on that object are not guaranteed to return the number of bytes you request. This article covers exactly that.
Use Extension Methods
We can use a couple of extension methods as a simple solution to perform exact length reads from Stream
objects. By "exact length reads", I mean that a guaranteed exact number of bytes are always read, or else an exception is thrown, hence I named the extension methods including the suffix "Exactly
".
Note: This article assumes you already know how to use extension methods in C#.
Blocking Extension Method
The following code is a simple extension method for Stream
objects. It allows you to read a guaranteed number of bytes from a Stream
object (synchronously). If the end of the Stream
is reached and the number of bytes read is not yet the amount you requested, an EndOfStreamException
is thrown.
public static void ReadExactly(this Stream stream,
byte[] buffer, int startIndex, int count)
{
if (stream is null)
throw new ArgumentNullException(nameof(stream));
if (buffer is null)
throw new ArgumentNullException(nameof(buffer));
if (startIndex < 0 || startIndex >= buffer.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0)
throw new ArgumentException(
"The number of bytes to read cannot be negative.", nameof(count));
if (startIndex + count > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(count),
$"'{nameof(count)}' is greater than the length of '{nameof(buffer)}'.");
if (!stream.CanRead)
throw new InvalidOperationException("Stream is not readable.");
int offset = 0;
while (offset < count)
{
int readCount = stream.Read(buffer, startIndex + offset, count - offset);
if (readCount == 0)
throw new EndOfStreamException("End of the stream reached.");
offset += readCount;
}
}
The above method example works just like the regular Stream.Read
method, but it performs multiple reads to ensure the number of bytes you request are always read and throws an exception if such isn't possible. The method above also performs parameter validation and throw the appropriate exceptions when parameter values are incorrect.
Asynchronous Extension Method
The previous method blocks the current thread while reading from a Stream
. The following method is very similar, but it can be called asynchronously using the Task Based Async Pattern.
public static async Task ReadExactlyAsync(this Stream stream,
byte[] buffer, int startIndex, int count)
{
if (stream is null)
throw new ArgumentNullException(nameof(stream));
if (buffer is null)
throw new ArgumentNullException(nameof(buffer));
if (startIndex < 0 || startIndex >= buffer.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0)
throw new ArgumentException(
"The number of bytes to read cannot be negative.", nameof(count));
if (startIndex + count > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(count),
$"'{nameof(count)}' is greater than the length of '{nameof(buffer)}'.");
if (!stream.CanRead)
throw new InvalidOperationException("Stream is not readable.")
int offset = 0;
while (offset < count)
{
int readCount = await stream.ReadAsync(buffer, startIndex + offset, count - offset);
if (readCount == 0)
throw new EndOfStreamException("End of the stream reached.");
offset += readCount;
}
}
ReadExactlyAsync
method calls the ReadAsync
implementation of the Stream
derived class. This means asynchronous performance depends on that implementation. For example, if the derived implementation of ReadAsync
instantiates threads (it usually shouldn't), a new thread would be instantiated per each read operation performed by the ReadExactlyAsync
method.
Using the Code
To consume the extension methods demonstrated above, you just need a Stream
. The following example shows how you would consume them:
using Stream someStream = GetSomeStream();
byte[] someBytes = new byte[1024];
someStream.ReadExactly(someBytes, 0, someBytes.Length);
await someStream.ReadExactlyAsync(someBytes, 0, someBytes.Length);
How you can observe, the consumption of the proposed extension methods is very similar to the standard Stream
methods.
Points of Interest
Although very simple to implement, writing code like this every time you need to read from a Stream
is not practical. Here, I suggested the use of extension methods, but any form of helper method should be ok as well. What is interesting about this matter is how often it is ignored by developers, either due to the way Stream
is designed to be consumed (i.e., the API is not suggestive enough and reading documentation is required) or because it requires more code to be written and more complexity to be added.
History
- 30th July, 2021 — Initial release