Stream Classes
Vaibhav • September 10, 2025
In the previous article, we explored how to read and write binary files using FileStream
, BinaryReader
, and BinaryWriter
. These classes gave us low-level control over byte-level operations.
But behind the scenes, nearly all file I/O in .NET is built on a powerful abstraction: streams.
In this article, we’ll dive into the stream class hierarchy, understand how streams work, and learn how to use
them effectively for both text and binary data.
What Is a Stream?
A stream is an abstraction for reading and writing a sequence of bytes. It could represent a file, a memory buffer, a network socket, or even a compressed archive. The key idea is that you don’t need to know the underlying source - you just read or write bytes through a consistent interface.
Streams are part of the System.IO
namespace and are designed to be composable.
You can wrap one stream inside another to add functionality like buffering, compression, or encryption.
All stream classes inherit from the abstract base class Stream
. This class
defines the core methods and properties for reading,
writing, seeking, and flushing data.
Common Stream Types
.NET provides several built-in stream types. Here are the most commonly used ones:
FileStream
- reads and writes filesMemoryStream
- reads and writes to memoryBufferedStream
- adds buffering to another streamNetworkStream
- reads and writes over a networkCryptoStream
- adds encryption/decryptionGZipStream
- compresses or decompresses data
Each of these streams serves a different purpose, but they all share the same interface defined by Stream
.
Basic Stream Operations
The Stream
class defines several key methods:
Read(byte[] buffer, int offset, int count)
Write(byte[] buffer, int offset, int count)
Seek(long offset, SeekOrigin origin)
Flush()
Close()
(viaDispose()
)
Let’s look at a simple example using FileStream
:
using FileStream fs = new FileStream("data.bin", FileMode.Create);
byte[] buffer = { 1, 2, 3, 4 };
fs.Write(buffer, 0, buffer.Length);
fs.Flush();
This creates a file and writes four bytes to it. Flush
ensures that all
buffered data is written to disk.
Reading with Streams
Reading from a stream involves filling a buffer with bytes. You can read in chunks or byte-by-byte:
using FileStream fs = new FileStream("data.bin", FileMode.Open);
byte[] buffer = new byte[4];
int bytesRead = fs.Read(buffer, 0, buffer.Length);
Console.WriteLine($"Read {bytesRead} bytes");
This reads up to 4 bytes from the file. The actual number of bytes read may be less if the file is smaller.
Using MemoryStream
MemoryStream
is useful when you want to work with data in memory instead of on
disk. It’s often used for temporary buffers or when working with APIs that expect a stream.
byte[] data = { 10, 20, 30 };
using MemoryStream ms = new MemoryStream(data);
int b = ms.ReadByte();
Console.WriteLine($"First byte: {b}");
This reads the first byte from the memory stream. You can also write to a MemoryStream
and retrieve the data using ToArray()
.
BufferedStream for Performance
BufferedStream
wraps another stream and adds a buffer to reduce the number of
I/O operations. This improves performance when reading or writing small chunks repeatedly.
using FileStream fs = new FileStream("data.bin", FileMode.Open);
using BufferedStream bs = new BufferedStream(fs);
byte[] buffer = new byte[1024];
int bytesRead = bs.Read(buffer, 0, buffer.Length);
This reads data through a buffer, which reduces disk access and improves speed.
Use BufferedStream
when reading or writing small amounts of
data frequently. It reduces overhead and improves performance.
StreamReader and StreamWriter
For text files, you’ll often use StreamReader
and StreamWriter
. These classes wrap a stream and handle character encoding, line
breaks, and buffering.
// Writing text
using FileStream fs = new FileStream("text.txt", FileMode.Create);
using StreamWriter writer = new StreamWriter(fs);
writer.WriteLine("Hello, Stream!");
// Reading text
using FileStream fs = new FileStream("text.txt", FileMode.Open);
using StreamReader reader = new StreamReader(fs);
string line = reader.ReadLine();
Console.WriteLine(line);
These classes automatically handle encoding and buffering, making them ideal for working with text files.
Composing Streams
One of the most powerful features of streams is composition. You can wrap streams inside other streams to add
functionality. For example, you can compress data using GZipStream
:
using FileStream fs = new FileStream("compressed.gz", FileMode.Create);
using GZipStream gzip = new GZipStream(fs, CompressionMode.Compress);
using StreamWriter writer = new StreamWriter(gzip);
writer.WriteLine("This is compressed text.");
This writes compressed text to a file. You can read it back using the same stream composition in reverse.
You can chain multiple streams together - for example, a BufferedStream
over a CryptoStream
over a
FileStream
. This lets you build powerful I/O pipelines.
Checking Stream Capabilities
Not all streams support all operations. You can check a stream’s capabilities using these properties:
CanRead
CanWrite
CanSeek
using FileStream fs = new FileStream("data.bin", FileMode.Open);
Console.WriteLine($"Readable: {fs.CanRead}");
Console.WriteLine($"Writable: {fs.CanWrite}");
Console.WriteLine($"Seekable: {fs.CanSeek}");
This helps you avoid runtime errors when using streams in generic code.
Disposing Streams Properly
Streams often hold unmanaged resources like file handles or network sockets. Always dispose them properly using
using
blocks or Dispose()
:
using FileStream fs = new FileStream("data.bin", FileMode.Open);
// Stream is automatically closed when the block ends
This ensures that resources are released promptly and avoids file locking or memory leaks.
Asynchronous Streams
Streams also support asynchronous operations for non-blocking I/O. This is useful in UI apps or servers where responsiveness matters.
using FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
This reads data asynchronously. You can also use WriteAsync
and FlushAsync
.
Use asynchronous streams in applications that require high responsiveness or handle many concurrent I/O operations.
Summary
Streams are the foundation of all I/O in .NET. They provide a unified interface for reading and writing data,
whether it’s from a file, memory, network, or another source. In this article, you learned about the Stream
class hierarchy, how to use FileStream
,
MemoryStream
, BufferedStream
, and how to
compose streams for advanced functionality. You also saw how to use StreamReader
and StreamWriter
for text, and how
to handle asynchronous operations. In the next article, we’ll explore path manipulation - how to work with file
and directory paths safely and portably across platforms.