Click here to Skip to main content
15,881,380 members
Articles / Programming Languages / C#
Tip/Trick

Reading Streams, The Exact Length Way

Rate me:
Please Sign up or sign in to vote.
4.33/5 (3 votes)
30 Jul 2021CPOL3 min read 8.9K   3   5
A demonstration about how to read an exact number of bytes from Stream objects
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.

C#
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.

C#
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:

C#
using Stream someStream = GetSomeStream(); // This requires C# 8.0.
byte[] someBytes = new byte[1024];

// Consume using the blocking method.
someStream.ReadExactly(someBytes, 0, someBytes.Length);

// Consume using the async method.
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Portugal Portugal
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraisePerfect demonstration of the fail fast principle Pin
sx20082-Aug-21 14:09
sx20082-Aug-21 14:09 
GeneralRe: Perfect demonstration of the fail fast principle Pin
AnotherKen3-Aug-21 15:55
professionalAnotherKen3-Aug-21 15:55 
PraiseClear and Simple Pin
mldisibio2-Aug-21 11:56
mldisibio2-Aug-21 11:56 
BugNot clear on how to use buffer: must read all of the stream Pin
AndyHo2-Aug-21 5:38
professionalAndyHo2-Aug-21 5:38 
GeneralRe: Not clear on how to use buffer: must read all of the stream Pin
Adérito Silva2-Aug-21 8:52
Adérito Silva2-Aug-21 8:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.