Files form the foundation of persistent data storage across applications. Mastering file I/O unlocks faster load times, robust recovery, and rich functionality. This 3021 word guide explores the core classes, advanced techniques, and key considerations every .NET developer should know.
Stream Fundamentals
The Stream abstract class underpins all file handling in .NET. It defines a sequence of bytes along with methods to read, write, seek and flush to an underlying storage medium like a hard disk or network connection.
FileStream then extends this for reading and writing files on disk:
FileStream file = File.Open("data.bin", FileMode.OpenOrCreate);
The constructor takes a file path and mode specifying how to open the file. Common modes include:
- Create: Creates a new file. Overwrites existing.
- CreateNew: Creates a new file. Errors if exists.
- Open: Opens file if it exists, else errors.
- OpenOrCreate: Opens file or creates new.
Readers and Writers
We attach helper streams for convenient reading and writing:
var reader = new StreamReader(file);
var writer = new StreamWriter(file);
writer.WriteLine("Hello world!");
Console.WriteLine(reader.ReadLine());
StreamReader handles text encoding and buffering while StreamWriter provides handy methods like WriteLine.
Always wrap streams in using blocks to dispose properly after use:
using (var stream = File.Open("file.bin", FileMode.OpenOrCreate))
{
// Use stream
} // Automatically disposed
Disposing releases unmanaged resources like file handles. Failing to dispose streams can leak OS resources.
Buffering, Asynchrony, Concurrency
Advanced stream capabilities optimize speed and concurrency:
Buffering collects data in memory before writing in bulk:
var stream = new BufferedStream(File.Open("log.txt", FileMode.Append));
stream.WriteLine("Log entry 1");
stream.WriteLine("Log entry 2"); // Buffered
Buffering reduces expensive disk seeks. Trade memory for faster writes.
Asynchrony executes I/O on background threads:
await stream.WriteAsync("Hello");
Async prevents UI blocking during long file operations.
Concurrency coordinates multithreaded access:
var options = new FileOptions(access: FileAccess.ReadWrite, sharing: FileShare.Read);
using (var stream = File.Open("file.txt", options))
{
// Concurrent read/write
}
Fine-grained control prevents race conditions between threads.
In summary:
- Buffering: Bulk collect data in memory for fewer writes
- Asynchrony: Avoid blocking UI threads with async operations
- Concurrency: Manage multithreaded access to files
Reading and Writing Files
Let‘s explore helper methods for common file I/O workflows:
Write Text
The File static class provides convenience helpers.
Write an entire string with WriteAllText():
File.WriteAllText("readme.txt", "This is the text contents");
Or write lines with WriteAllLines():
string[] lines = { "line 1", "line 2" };
File.WriteAllLines("file.txt", lines);
Both overwrite existing files if present.
Append Text
To add to a file, use AppendAllText():
File.AppendAllText("log.txt", $"Logged {DateTime.Now}");
This opens the file, seeks to the end, writes the text, and closes implicitly.
For manual control, create a StreamWriter:
using (var writer = File.AppendText("log.txt"))
{
writer.WriteLine("Error!");
writer.WriteLine(DateTime.Now);
}
Don‘t forget to dispose writers when done.
Read Text
Complementary methods read file contents:
string text = File.ReadAllText("instructions.txt");
string[] lines = File.ReadAllLines("cli.history");
Bonus: Calculate lines of code:
var lines = File.ReadAllLines("Program.cs");
Console.WriteLine($"{lines.Length} lines!");
Binary Serialization
Text is human-readable but binary is more compact and performant. serialized binary data integrates neatly into C#‘s object-oriented approach.
The two main formats:
- JSON: Widely supported. Human readable.
- Protocol Buffers: Space and speed efficient. Not human readable.
Let‘s compare them.
JSON Serialization
JSON is ubiquitous for web and mobile apps. In .NET, System.Text.Json provides high performance cooked into the runtime.
Serialize any object graph into JSON:
public class GameState
{
public int Level { get; set; }
public string PlayerName { get; set; }
public Vector Position { get; set; }
}
var state = new GameState();
string json = JsonSerializer.Serialize(state);
File.WriteAllText("state.json", json);
Deseralize back to objects:
string json = File.ReadAllText("state.json");
var state = JsonSerializer.Deserialize<GameState>(json);
Json.NET also supports advanced serialization scenarios.
Protocol Buffers
For the ultimate in speed and size, Google‘s Protocol Buffers (protobufs) offer highly efficient cross-platform serialization tightly integrated with .NET.
Define schema in .proto files:
message GameState {
int32 level = 1;
string player_name = 2;
Vector position = 3;
}
Compile to C# classes with the protobuf compiler then serialize:
var state = new GameState();
state.Level = 10;
using (var stream = File.Open("state.dat", FileMode.Create))
{
Serializer.Serialize(stream, state);
}
Deserialization follows symmetrically.
Performance benchmarks show protobufs substantially faster than JSON, especially on mobile and WebAssembly.
Asynchronous I/O
Synchronous file I/O blocks executing threads, harming responsiveness. Async methods lift this burden:
// Task-based async
Task WriteDataAsync(byte[] data)
{
using (var stream = File.Open("data.bin", FileMode.Create)) {
await stream.WriteAsync(data);
}
}
// Async streams
using (var stream = new FileStream("data.bin", FileMode.Create))
{
await stream.WriteAsync(buffer);
}
Async code scales elegantly without blocking precious UI threads.
Behind the scenes, async relies on OS IOCP support for non-blocking disk operations.
Memory Mapped Files
Memory-mapped files treat disk contents as directly accessible memory for ultra fast access. Useful for very large data.
using (var mmap = MemoryMappedFile.CreateFromFile("giant.dat"))
{
var view = mmap.CreateViewAccessor();
for (long i = 0; i < view.Length; i++)
{
view.Write(i, (byte) 1);
}
}
Internally, the OS manages efficient on-demand paging without copying entire files into RAM.
Embedded databases like SQLite also employ memory mapping.
Choosing File Locations
Where should data be written? Standard OS folders help:
| Location | Description |
|---|---|
Environment.SpecialFolder.ApplicationData |
Per-user app data |
Environment.SpecialFolder.LocalApplicationData |
Local low-rights app data |
Environment.SpecialFolder.Temp |
Temporary files – deleted occasionally |
Path.GetTempPath() |
Temporary files – deleted occasionally |
Examples:
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string tempPath = Path.GetTempPath();
Special folders abstract OS differences. Avoid hard coding paths!
Security Best Practices
File I/O capabilities require careful permissions management to prevent access violations or compromise.
Validate all user input to guard against injection attacks smuggling unexpected path characters.
Apply principle of least privilege when specifying access requirements in app manifests rather than blanket full access.
Never hardcode credentials like API keys in files – use the OS protected credential storage instead.
Encrypt sensitive data written to disk to prevent exposure.
Adopting these design habits from the outset radically improves the security posture of your applications.
Benchmarking File Write Speeds
How fast can we realistically write files? Benchmarks provide insight:
Disk Type | Sequential Write Speed
———————————————————————
HDD | 80-150 MB/sec
SATA SSD | 500 MB/sec
NVMe SSD | 1500-2500 MB/sec
That‘s raw disk speed. In practice, software overheads like buffers and schedulers introduce latency bringing speeds down to ~50MB/sec for HDD and ~250MB/sec for SATA SSD.
Maximize throughput by:
- Writing larger chunk sizes (64KB ideal)
- Using async I/O
- Spanning multiple files across disks
- Compressing data
Delivering data faster keeps users happy!
Portable Path Manipulation
Windows systems employ backslashes ( \ ) in file paths while Linux and macOS use forward slashes ( / ).
To transparently support multi-platform apps, use System.IO.Path:
GOOD: Portable Paths
string dataFolder = Path.Combine( rootPath, "data" );
if (!Directory.Exists(dataFolder)) {
Directory.CreateDirectory( dataFolder );
}
string filePath = Path.Combine(dataFolder, "stats.bin");
BAD: Hardcoded Windows Paths
string statsFile = "C:\App\Data\stats.bin";
// Won‘t work on Linux!
Bonus: URIs abstract file locations for distributed systems:
file:///opt/data/config.json
http://archive.org/data.zip
ftp://files.mooseindustries.com
So always favor Path and URIs over hardcoded strings!
Summing Up
We‘ve covered a lot of ground across C#‘s file I/O landscape:
- Streams and readers/writers form the foundation
- Buffering reduces disk operations
- Async performs I/O in the background
- Serialization converts objects to persistent formats
- Special folders store app data conveniently
- Security best practices keep data safe
- Path manipulation libraries enable portability
File I/O mastery will serve you well as you build robust, smooth and featurerich .NET applications.
Now get coding!


