C# Streams, StreamReader, and StreamWriter Explained

In C# and the .NET runtime, input and output (I/O) operations rely on streams to transfer data between memory, files, and network sockets. This article explains the architectural differences and use cases between the base Stream class, which handles raw binary byte sequences, and the helper classes StreamReader and StreamWriter, which abstract character encoding to process human-readable text.

The Foundation: The Stream Class

The System.IO.Stream class is an abstract base class that represents a contiguous sequence of bytes. It provides a generic view of a sequence of bytes, regardless of the underlying backing store (such as a file, memory buffer, or network connection).

Common implementations include:

  • FileStream: Reads and writes bytes to and from physical files.
  • MemoryStream: Reads and writes bytes to and from system memory as a backing store.
  • NetworkStream: Facilitates byte transfer across network sockets.

Streams provide core methods such as Read(), Write(), Seek(), and Flush(). Because they operate strictly on byte arrays (byte[]), raw streams are primarily used for binary I/O—tasks such as transmitting raw packets, reading serialized binary formats, image manipulation, or parsing compressed archives.

Binary I/O with Streams

When performing binary operations, you work with raw bytes without encoding conversions. To simplify reading and writing primitive data types (such as int, bool, or double) to a binary stream, .NET provides BinaryReader and BinaryWriter. These classes wrap an existing Stream object and read or write primitive values directly as binary data, preserving the exact bit-level representation.

Text I/O: StreamReader and StreamWriter

While raw streams manipulate bytes, human-readable text consists of characters and strings. Because a single character can span anywhere from one to four bytes depending on the encoding (e.g., UTF-8, UTF-16, ASCII), handling text via raw byte arrays is error-prone.

StreamReader and StreamWriter solve this problem by wrapping an underlying Stream and automatically translating between byte representations and C# char or string types.

StreamReader

StreamReader inherits from TextReader and reads characters from a byte stream using a specified character encoding (defaulting to UTF-8).

  • Use Case: Reading log files, configuration files, CSVs, or text payloads.
  • Key Methods:
    • ReadLine(): Reads a single line of characters and returns it as a string.
    • ReadToEnd(): Reads all characters from the current position to the end of the stream.
    • Read(): Reads the next single character or a block of characters.
using (FileStream fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read))
using (StreamReader reader = new StreamReader(fs))
{
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        // Process each line of text
    }
}

StreamWriter

StreamWriter inherits from TextWriter and writes characters and strings to a byte stream, converting them into bytes using the chosen encoding.

  • Use Case: Generating text files, writing structured logs, or formatting plain text output.
  • Key Methods:
    • Write(): Writes text without appending a line terminator.
    • WriteLine(): Writes text followed by the environment-specific line terminator.
    • Flush(): Clears all buffers and causes buffered data to be written to the underlying stream.
using (FileStream fs = new FileStream("output.txt", FileMode.Create, FileAccess.Write))
using (StreamWriter writer = new StreamWriter(fs))
{
    writer.WriteLine("Hello, World!");
    writer.WriteLine("Second line of text.");
}

Key Differences Between Binary and Text I/O

Feature Streams / BinaryReader / BinaryWriter StreamReader / StreamWriter
Data Unit Bytes (byte, byte[]) and binary primitives Characters (char, string)
Encoding None; processes raw bytes directly Requires character encoding (UTF-8, ASCII, etc.)
Performance Faster; zero overhead from string parsing Minor overhead due to decoding/encoding characters
Target Data Images, compiled files, network packets, binary formats JSON, XML, CSV, plain text documents, logs

Resource Management

All stream-related classes implement the IDisposable interface. Wrapping them in using statements ensures that unmanaged operating system handles, file locks, and network sockets are closed and released immediately when the operation concludes, while also ensuring that buffered output data is properly flushed. Closing an outer reader or writer automatically closes the underlying stream it wraps.