1. Introduction
Let's imagine a kettle with water. You open the tap — water starts to flow. You can fill up a lot of water and pour it all at once, or you can fill the kettle little by little. It's the same with files — it's not always convenient or even possible to load the whole file into memory at once. Files can be huge, and sometimes your data source isn't even a file, but, say, a network connection where data comes in gradually.
If we always tried to work just with byte arrays, we'd run out of memory super fast on big files, and for "endless" data streams (like video or audio streams), this approach just wouldn't work. That's where the stream concept comes to the rescue!
In .NET, a stream is an abstraction for sequential access to data: it doesn't matter what's behind the source — a file, network, memory, or even something totally exotic like a compressed archive. A stream lets you read and write data in parts, usually in blocks or bytes.
Main idea:
- Stream — it's a channel for transferring data. It's like a conveyor belt: you can "put" (write) or "take" (read) data, without worrying about the details of where and how it's stored.
- Data comes in sequence: you can only read the next chunk after the previous one (or the other way around, if seeking is supported).
- Most of the time, you don't keep all the data in memory at once (and your computer will thank you for that).
This abstraction is at the core of pretty much all input/output operations in .NET: working with files, networks, archives, even the console!
2. Streams System.IO.Stream
Inheritance and architecture: System.IO.Stream
Almost all streams in .NET inherit from the abstract class System.IO.Stream. It defines the main methods for reading, writing, seeking, and managing the stream.
- Stream — base abstract class
- FileStream — for working with files
- MemoryStream — for working with data in memory
- NetworkStream — for network interaction
- CryptoStream — for encryption/decryption
Quick intro to key stream properties and methods
| Property / Method | Description |
|---|---|
|
Can you read from this stream |
|
Can you write to this stream |
|
Can you move around in the stream (not all support this) |
|
Stream length (if supported — not all streams have this) |
|
Current position in the stream |
|
Read data |
|
Write data |
|
Move around in the stream |
|
Flush the buffer (write all accumulated data to the stream) |
/ |
Close the stream and free up resources |
Let's see what this looks like "in practice".
3. Example: reading and writing files with Stream
Here's a pretty minimal example to see a stream "in action":
// Open a file for writing
using var stream = new FileStream("numbers.bin", FileMode.Create);
// Let's say we want to write numbers from 1 to 10 into the file
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // Write one byte at a time
}
// Explicitly close the file so we can open it for reading
stream.Close();
// Now let's try to read those numbers back
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // Will print 1, 2, ... 10
}
Here we're using FileStream, which is a real stream in every sense: you read and write data in blocks or by bytes.
Types of streams: where can you find them?
A stream isn't just a file on disk. Here are a few examples where the stream concept is used:
- File on disk (for example, FileStream — the most common case)
- Stream in RAM (MemoryStream — handy for temporary or intermediate data)
- Network connection (NetworkStream)
- Compression/archiving (GZipStream, DeflateStream)
- Encryption (CryptoStream)
- Console input/output (yep!) — technically, also streams
This lets you write code without worrying about the specific data source/target: if your code works with a stream, it's universal!
4. Handy details
Reading and writing are operations of transferring data in parts. Usually through byte arrays and the Read, Write methods.
Example: reading a file in blocks
byte[] buffer = new byte[1024]; // Buffer for 1024 bytes (1 KB)
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// Process only bytesRead bytes inside buffer
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Block sum: {sum}");
}
This approach is used everywhere — from antivirus software to music players.
Seeking in a stream (Position, Seek)
In most stream implementations (like file streams), you can move around in the data — not just read the "next chunk", but jump to a specific position and work with the data from there.
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Move to the 6th byte (indexing from 0)
int value = stream.ReadByte();
Console.WriteLine($"6th byte in file: {value}");
Streams can be read-only, write-only, or both
Some streams only support one of the options:
- File opened for writing: only Write()
- Stream for reading network data: only Read()
- In some exotic cases (like a stream for printing to a printer) "rewind" or seeking is impossible (can't go back).
Check supported operations using the CanRead, CanWrite, CanSeek properties:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("Reading is supported");
if (stream.CanWrite)
Console.WriteLine("Writing is supported");
if (stream.CanSeek)
Console.WriteLine("Seeking in file is supported");
Buffering in streams
Almost all streams use internal buffers to boost performance. Buffering saves disk/network calls: data is collected internally, then sent/written in batches.
The Flush() method lets you flush the buffer (for example, to make sure everything is written to disk):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // Guarantees the write actually hits the disk
If you're writing mission-critical data (like payment transactions!), calling Flush() is your buddy.
5. Typical mistakes when working with streams
Newbies very often make these mistakes:
They forget to close the stream (and get memory leaks, "stuck" files, and all sorts of fun stuff).
They mix up text and binary streams — try to write a string with a byte method, then get "gibberish".
They use a buffer that's too small (or no buffer at all) — operations get slow.
They think Read() always reads exactly as many bytes as you ask for — actually, it can return less; you always need to check the return value.
They don't realize not all streams support seeking (Seek), especially network ones.
For example:
// Bad example: reading all file bytes without checking how many were actually read
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// bytesRead can be less than 1024 if the file is smaller!
}
GO TO FULL VERSION