Files Streams
By the end of this lesson you'll be able to read and write data with C#'s stream types — StreamReader , StreamWriter , BufferedStream and MemoryStream — dispose them cleanly with using , and understand why flushing and rewinding matter. These are the tools behind every file, network and pipeline read in .NET.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
A stream is a conveyor belt in a factory. Data flows past you piece by piece — you never need to see the whole shipment at once, which is how a program can process a file far bigger than its memory. A buffer is the loading dock beside the belt: items pile up there and get moved in one bulk trip instead of one box at a time, which is far more efficient. A StreamWriter is the worker placing boxes on the belt; a StreamReader is the worker taking them off. And Flush is shouting "send what's on the dock right now!" — until you do, the last few boxes may still be sitting there, not yet on the belt.
Every I/O type in .NET is built on the abstract Stream class — a flow of bytes you can read, write, and (sometimes) seek within. Different streams have different sources , and helper classes layer text or buffering on top. Here's the cast you'll meet in this lesson.
The clever part: because they all share the Stream base type, a StreamReader doesn't care whether it's reading a file, a network socket, or memory. Learn the pattern once and it works everywhere.
1. What a Stream Is — Write, Flush, Rewind, Read
A stream is a one-way flow of bytes with an internal cursor marking your current position. You write bytes in or read bytes out, and the cursor moves along as you go. A StreamWriter sits on top and converts your text into bytes; a StreamReader converts bytes back into text. To keep this lesson runnable without a real file, we use a MemoryStream — a stream whose source is just an array in memory. Read this worked example carefully: it shows the full round trip and the two steps beginners forget — Flush and rewinding the cursor.
Your turn. The program below does the same round trip but is missing two pieces. Fill in the two ___ blanks using the hints, then run it. If you see nothing printed, you forgot to rewind.
2. Reading & Writing Files — Helpers and StreamReader
For small files, .NET gives you one-line helpers: File.WriteAllText replaces a file's contents, and File.ReadAllText / File.ReadAllLines pull it all back. They're perfect for config files and quick scripts. But they load the entire file into memory at once, so for large files you read line by line with a StreamReader inside a using block. The using is not optional politeness — it guarantees the operating-system file handle is closed even if an exception is thrown halfway through.
Now you try. The buffer below already has three lines written into it; your job is to read them back and process them by counting how many there are. Fill in the two ___ blanks:
3. Buffering — Doing Fewer, Bigger Trips
Every individual read or write to a raw stream can cost a trip to the disk or network — and those trips have fixed overhead, so a thousand tiny writes are far slower than a few big ones. A BufferedStream wraps another stream and accumulates small operations in memory, flushing them out in large chunks. You don't change what gets written — only how efficiently . (Note: StreamWriter and FileStream already buffer internally, so you mainly add a BufferedStream around streams that don't, such as a raw network stream.)
Streams hold unmanaged resources — OS file handles, network sockets, locked buffers. The garbage collector cleans up normal C# objects, but it can't promptly release these. That's what IDisposable and using are for: the moment a using block ends, Dispose() runs, which flushes any buffered data and frees the handle — even if an exception was thrown.
Disposing a StreamWriter normally flushes and then closes the underlying stream too . That's why the examples pass leaveOpen: true — so disposing the writer flushes the text but leaves the MemoryStream open for us to rewind and read.
Two failure modes to remember: forget to Flush /dispose and your last writes never reach the stream; forget to set Position = 0 and the reader starts at the end, reading nothing.
Here's a small but real program that uses everything from this lesson at once — a MemoryStream built straight from text, a StreamReader in a using , line-by-line reading, and parsing each line into a total. This is exactly how you'd process a real CSV file; only the source would change from memory to a FileStream .
Swapping new MemoryStream(...) for File.OpenRead("orders.csv") turns this into a real file reader — the StreamReader loop doesn't change a single line. That's the power of programming against the Stream abstraction.
When you need the absolute highest throughput — a web server parsing millions of request bodies, say — even buffered streams leave performance on the table. System.IO.Pipelines is a newer API built for exactly that. A Pipe has a PipeWriter and a PipeReader that share a pool of memory buffers, giving you zero-copy reads (no shuffling bytes between arrays), automatic back-pressure (the writer slows down if the reader falls behind), and far less garbage-collector pressure.
It's the engine inside Kestrel, ASP.NET Core's web server. You won't reach for it in everyday code — a StreamReader is simpler and plenty fast — but it's the tool to know about the day a profiler says stream parsing is your bottleneck.
Q: When should I use File.ReadAllText instead of a StreamReader ?
Use File.ReadAllText for small files where loading everything at once is fine — config, small data files, quick scripts. Switch to a StreamReader with ReadLine() when the file is large, so you process one line at a time instead of pulling gigabytes into memory.
Q: Why does my MemoryStream read return nothing?
Almost always because you didn't rewind. Writing leaves the cursor at the end of the stream; reading from there yields nothing. Set ms.Position = 0; after writing and before reading.
Q: Do I really need Flush() if I'm using using ?
Disposing (which using does at the end of the block) flushes for you, so an explicit Flush() is only needed when you want the data pushed out before the block ends — for example, to rewind and read the same stream straight away.
Q: What's the difference between a Stream and a StreamReader ?
A Stream deals in raw bytes . A StreamReader / StreamWriter wraps a stream and handles the text encoding for you, so you work with strings and lines instead of byte arrays.
Q: When would I use System.IO.Pipelines over streams?
Only in high-throughput, performance-critical code — parsing huge volumes of network data, for instance. For everyday file work a StreamReader is simpler and fast enough; Pipelines trades that simplicity for zero-copy reads and back-pressure.
No blanks this time — just a brief and an outline to keep you on track. Read the in-memory buffer of numbers line by line, total them, count them, and print the average. Run it and check your output against the expected line in the comments. This is the same loop you'd use over a real file of readings or scores.
Practice quiz
What is a Stream in .NET?
- A one-way flow of bytes you read from or write to
- A fixed-size array of characters
- A type that holds the whole file in memory at once
- A thread that runs file I/O in the background
Answer: A one-way flow of bytes you read from or write to. A stream is a one-way flow of bytes from a source (file, network, or memory) with an internal cursor marking your position.
After writing to a MemoryStream, what must you do before reading the data back?
- Call Encoding.UTF8.GetBytes()
- Set ms.Position = 0 to rewind the cursor
- Dispose the MemoryStream first
- Call ms.Clear()
Answer: Set ms.Position = 0 to rewind the cursor. Writing leaves the cursor at the end. You must set Position = 0 to rewind, or the reader starts at the end and reads nothing.
What does StreamWriter.Flush() do?
- Closes the underlying stream
- Deletes the file on disk
- Pushes buffered text out into the stream now
- Resets the cursor to position 0
Answer: Pushes buffered text out into the stream now. Flush pushes buffered text into the stream immediately. Disposing the writer also flushes, but Flush lets you do it before the block ends.
Why wrap a stream in a using statement?
- It makes reads faster
- It converts bytes to text automatically
- It rewinds the cursor automatically
- Dispose runs at the end of the block, flushing and freeing the handle even if an exception is thrown
Answer: Dispose runs at the end of the block, flushing and freeing the handle even if an exception is thrown. Streams hold unmanaged resources. using guarantees Dispose() runs (flushing and closing the handle) even when an exception is thrown.
What does a BufferedStream do?
- Encrypts the data as it passes through
- Batches many small reads/writes into fewer big ones
- Keeps the whole file in memory
- Converts the stream to a string
Answer: Batches many small reads/writes into fewer big ones. A BufferedStream wraps another stream and accumulates small operations, flushing them in large chunks to reduce expensive trips to the source.
What is the advantage of reading a large file line by line with StreamReader.ReadLine() instead of File.ReadAllText?
- It sorts the lines automatically
- It only processes one line at a time, avoiding loading the whole file into memory
- It is the only way to read text files
- It encrypts each line
Answer: It only processes one line at a time, avoiding loading the whole file into memory. ReadAllText loads the entire file into memory. ReadLine() processes one line at a time, so it scales to files far larger than available memory.
When does StreamReader.ReadLine() signal the end of the stream?
- It returns an empty string
- It returns null
- It throws EndOfStreamException
- It returns -1
Answer: It returns null. ReadLine() returns null at the end of the stream, which is why the read loop is while ((line = reader.ReadLine()) != null).
What does leaveOpen: true do when passed to a StreamWriter?
- Keeps the underlying stream open after the writer is disposed
- Disables buffering
- Leaves the file unlocked for other processes
- Prevents the writer from flushing
Answer: Keeps the underlying stream open after the writer is disposed. Disposing a StreamWriter normally closes the underlying stream too. leaveOpen: true keeps it open so you can rewind and read it back.
Which stream type is backed by a byte array in RAM and needs no disk?
- FileStream
- BufferedStream
- MemoryStream
- NetworkStream
Answer: MemoryStream. MemoryStream is a stream backed by a byte array in memory — ideal for tests and buffers where no real file is needed.
What does System.IO.Pipelines provide that buffered streams do not?
- Automatic file encryption
- Zero-copy reads and automatic back-pressure
- A simpler API than StreamReader
- Synchronous-only I/O
Answer: Zero-copy reads and automatic back-pressure. Pipelines gives zero-copy reads, automatic back-pressure, and less GC pressure — the high-throughput engine inside Kestrel.