C# Story

Chapter #11 – Streams Library

System.IO: streams, readers, writers, paths

11.0 Prologue

The System.IO namespace provides the building blocks for reading and writing data: abstract streams, concrete file and memory streams, text readers and writers, and helper types for working with paths and directories.

11.1 The Stream Abstraction

System.IO.Stream is the abstract base for all I/O streams. Key members:
  • Read(byte[] buf, int offset, int count) — read bytes
  • Write(byte[] buf, int offset, int count) — write bytes
  • Seek(long offset, SeekOrigin origin) — random-access positioning
  • Flush() — force buffered data to the underlying sink
  • ReadAsync / WriteAsync — async overloads (preferred for I/O-bound work)
  • CanRead, CanWrite, CanSeek, Length, Position — properties
Concrete stream types include:
  • FileStream — file I/O
  • MemoryStream — in-memory byte buffer
  • NetworkStream — TCP socket I/O
  • GZipStream / DeflateStream — compression wrappers
  • CryptoStream — encryption/decryption wrapper
  • BufferedStream — adds buffering to an unbuffered stream
Always dispose streams — use a using statement or declaration: using var fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read); byte[] buf = new byte[fs.Length]; int bytesRead = fs.Read(buf, 0, buf.Length);

11.2 FileStream

FileStream accepts a FileMode and optional FileAccess and FileShare arguments:
FileMode Behavior
OpenOpen existing; exception if absent
OpenOrCreateOpen if present, create if absent
CreateCreate new (truncate if exists)
CreateNewCreate new; exception if exists
AppendOpen/create; position at end; no seek before end
TruncateOpen existing and truncate to zero bytes
// Write binary data using var ws = new FileStream("out.bin", FileMode.Create, FileAccess.Write); ws.Write(BitConverter.GetBytes(42)); // write int as 4 bytes ws.Write(BitConverter.GetBytes(3.14)); // write double as 8 bytes

11.3 Text Readers and Writers

StreamReader and StreamWriter wrap any stream with character encoding (default UTF-8). They provide line-oriented methods: // Write text using var sw = new StreamWriter("log.txt", append: true); sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] started"); // Read text using var sr = new StreamReader("log.txt"); string? line; while ((line = sr.ReadLine()) is not null) Console.WriteLine(line); Convenience helpers on the File static class avoid manual stream management for simple cases: // one-shot reads string all = File.ReadAllText("data.txt"); string[] lines = File.ReadAllLines("data.txt"); byte[] bytes = File.ReadAllBytes("data.bin"); // one-shot writes File.WriteAllText("out.txt", "hello\n"); File.AppendAllText("out.txt", "world\n"); // lazy line-by-line (avoids loading entire file) foreach (string ln in File.ReadLines("big.txt")) Process(ln); StringReader and StringWriter apply the same reader/writer interface to in-memory strings, making it easy to unit-test code that works with a TextReader or TextWriter without touching the filesystem.

11.4 BinaryReader and BinaryWriter

BinaryWriter writes primitive values in their binary representation. BinaryReader reads them back. They are type-safe and endian-consistent (always little-endian on .NET): // write using var bw = new BinaryWriter(File.Create("data.bin")); bw.Write(42); bw.Write(3.14); bw.Write("hello"); // length-prefixed UTF-8 string // read using var br = new BinaryReader(File.OpenRead("data.bin")); int n = br.ReadInt32(); double d = br.ReadDouble(); string s = br.ReadString();

11.5 MemoryStream

MemoryStream is backed by a resizable byte array. Use it to build up a byte sequence in memory and then use its ToArray() or GetBuffer() method to get the result, or seek back to the beginning and wrap it with another reader: using var ms = new MemoryStream(); using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) { bw.Write(100); bw.Write("test"); } ms.Position = 0; using var br = new BinaryReader(ms); Console.WriteLine(br.ReadInt32()); // 100 Console.WriteLine(br.ReadString()); // test

11.6 Path and Directory Helpers

System.IO.Path manipulates path strings without touching the filesystem. Directory and File operate on the filesystem: string dir = Path.GetDirectoryName("C:/data/log.txt")!; // C:/data string name = Path.GetFileName("C:/data/log.txt"); // log.txt string ext = Path.GetExtension("C:/data/log.txt"); // .txt string full = Path.Combine("C:/data", "sub", "out.txt"); // cross-platform join Directory.CreateDirectory("output/2026"); bool exists = File.Exists("config.json"); string[] files = Directory.GetFiles(".", "*.cs", SearchOption.AllDirectories); Use Path.Combine rather than string concatenation so that your code works on both Windows (backslash) and Unix (forward slash) without modification.

11.7 Async File I/O

File I/O is I/O-bound. Prefer async APIs so the calling thread is not blocked while the OS moves data: async Task WriteLogAsync(string path, string message) { await using var sw = new StreamWriter(path, append: true); await sw.WriteLineAsync($"[{DateTime.UtcNow:O}] {message}"); } async Task<string> ReadFileAsync(string path) => await File.ReadAllTextAsync(path); Note await using: when a disposable type also implements IAsyncDisposable, use await using to await the async dispose, which flushes the buffer before releasing the file handle.

11.8 Epilogue

This chapter covered the stream abstraction, file and memory streams, text and binary readers/writers, path utilities, and async I/O. The next chapter examines System.Collections.Generic.

11.9 References

Stream — Microsoft docs
FileStream — Microsoft docs
StreamReader — Microsoft docs
File and stream I/O overview