A stream is an abstraction over a sequence of bytes — it could come from a file, a network socket, a memory buffer, or an HTTP request body. The Stream base class provides Read(), Write(), Seek(), and Position, and derived classes provide the source. StreamReader and StreamWriter layer text encoding/decoding on top of a byte stream. Understanding streams is essential for efficient file processing in ASP.NET Core — handling large uploads, serving large downloads, and processing log files without loading everything into memory.
StreamReader and StreamWriter
// ── StreamReader — text reading with encoding control ─────────────────────
using var reader = new StreamReader("data.csv", Encoding.UTF8);
// Read entire file
string all = reader.ReadToEnd();
// Read line by line (memory-efficient for large files)
while (!reader.EndOfStream)
{
string? line = reader.ReadLine();
if (line is not null)
ProcessLine(line);
}
// ── StreamWriter — text writing ───────────────────────────────────────────
using var writer = new StreamWriter("output.csv", append: false, Encoding.UTF8);
writer.WriteLine("Id,Name,Email");
writer.WriteLine("1,Alice,alice@example.com");
writer.Flush(); // ensure buffered data is written to the underlying stream
// ── File.OpenText / File.CreateText — convenience factory methods ─────────
using var reader2 = File.OpenText("input.txt"); // → StreamReader with UTF-8
using var writer2 = File.CreateText("output.txt"); // → StreamWriter, create/overwrite
Note: Always wrap streams in
using statements (or using var). Streams hold unmanaged operating system resources — file handles — that must be released promptly. If a StreamReader is not disposed, the OS file handle stays open. On Windows, other processes cannot open the same file for writing while you hold a handle. The using statement calls Dispose() automatically when the variable goes out of scope, even if an exception is thrown. Forgetting using is one of the most common resource leak bugs in C# code.Tip: Specify the encoding explicitly when creating
StreamReader or StreamWriter. The default encoding varies by operating system — on Windows it may default to the system ANSI code page (Windows-1252), while on Linux it defaults to UTF-8. Always specify Encoding.UTF8 for cross-platform files, or new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) for UTF-8 without a BOM (Byte Order Mark), which is preferred for files used in web applications where BOM can cause issues with JavaScript parsers.Warning:
StreamReader.ReadLine() returns null when the end of the stream is reached — not an empty string. Always check for null: string? line = reader.ReadLine(); if (line is not null). Also be aware that StreamReader buffers data internally for performance — after reading from a FileStream through a StreamReader, the underlying FileStream.Position may be ahead of where the StreamReader logically is. Do not mix direct FileStream reads and StreamReader reads on the same stream.FileStream — Binary File Access
// FileStream — direct byte-level access
using var fs = new FileStream(
"binary.dat",
FileMode.OpenOrCreate, // create if not exists, open if exists
FileAccess.ReadWrite, // open for both reading and writing
FileShare.None); // no other process can open this file
// Write bytes
byte[] header = new byte[] { 0x89, 0x50, 0x4E, 0x47 }; // PNG magic bytes
fs.Write(header, 0, header.Length);
// Seek to position
fs.Seek(0, SeekOrigin.Begin); // go to start
// Read bytes into buffer
var buffer = new byte[4];
int bytesRead = fs.Read(buffer, 0, buffer.Length);
Console.WriteLine($"Read {bytesRead} bytes");
MemoryStream — In-Memory Buffer
// MemoryStream — acts like a file stream but stored in RAM
// Useful for building byte arrays, piping data between components
using var ms = new MemoryStream();
using var writer = new StreamWriter(ms, Encoding.UTF8, leaveOpen: true);
writer.WriteLine("Line 1");
writer.WriteLine("Line 2");
writer.Flush();
ms.Position = 0; // rewind before reading
using var reader = new StreamReader(ms);
string content = reader.ReadToEnd();
Console.WriteLine(content);
// Get bytes without a reader
byte[] bytes = ms.ToArray(); // independent copy of all bytes in the stream
Common Mistakes
Mistake 1 — Forgetting using on StreamReader/StreamWriter (resource leak)
❌ Wrong — file handle stays open indefinitely:
var reader = new StreamReader("file.txt");
string content = reader.ReadToEnd();
// Dispose never called — file handle leaked!
✅ Correct:
using var reader = new StreamReader("file.txt");
string content = reader.ReadToEnd(); // ✓ disposed at end of scope
Mistake 2 — Not rewinding MemoryStream before reading (reads 0 bytes)
❌ Wrong — position is at the end after writing, reading returns empty:
ms.Write(data);
var bytes = ms.ToArray(); // ✓ works (ToArray ignores position)
var text = new StreamReader(ms).ReadToEnd(); // ✗ returns "" — position is at end!
✅ Correct — reset position before reading:
ms.Position = 0; // or ms.Seek(0, SeekOrigin.Begin)
var text = new StreamReader(ms).ReadToEnd(); // ✓