Lesson 147 min read

File I/O

Read and write files — your program's long-term memory

Why Files Matter

Variables disappear when your program ends — they live in RAM, which gets wiped. Files are your program's long-term memory. They let you save data to disk so it survives restarts, crashes, and even power outages.

C# makes file operations surprisingly easy. The File class has simple one-liner methods for common tasks, and StreamReader/StreamWriter give you fine-grained control for bigger files.

All file operations live in the System.IO namespace (included by default in top-level statements).

Quick File Operations with the File Class

string path = "hello.txt";
// Write an entire file in one line
File.WriteAllText(path, "Hello from C#!\nThis is line 2.");
Console.WriteLine("File written!");
// Read the entire file in one line
string content = File.ReadAllText(path);
Console.WriteLine($"Content:\n{content}");
// Write lines (array of strings)
string[] shoppingList = { "Apples", "Bread", "Milk", "Eggs" };
File.WriteAllLines("shopping.txt", shoppingList);
// Read lines back as array
string[] lines = File.ReadAllLines("shopping.txt");
Console.WriteLine($"\nShopping list ({lines.Length} items):");
for (int i = 0; i < lines.Length; i++)
Console.WriteLine($" {i + 1}. {lines[i]}");
// Append to a file (adds to the end without erasing)
File.AppendAllText("shopping.txt", "\nCheese\nButter");
Console.WriteLine($"\nAfter appending:");
Console.WriteLine(File.ReadAllText("shopping.txt"));
// Check if file exists
Console.WriteLine($"\nhello.txt exists: {File.Exists(path)}");
Console.WriteLine($"ghost.txt exists: {File.Exists("ghost.txt")}");
Output
File written!
Content:
Hello from C#!
This is line 2.

Shopping list (4 items):
  1. Apples
  2. Bread
  3. Milk
  4. Eggs

After appending:
Apples
Bread
Milk
Eggs
Cheese
Butter

hello.txt exists: True
ghost.txt exists: False

StreamReader & StreamWriter — For Bigger Files

The File.ReadAllText() method loads the entire file into memory at once. That's fine for small files, but what if your file is 2 GB? Your app would eat all the RAM and crash.

StreamReader and StreamWriter work like a garden hose instead of a bucket. Instead of dumping all the water (data) at once, they flow it through a stream, using an internal buffer one piece at a time. Much more memory-friendly.

The using statement is critical here — it automatically closes the file when you're done, even if an exception occurs. Think of it as a "please put this back when you're finished" note.

StreamReader, StreamWriter & using

// Write with StreamWriter + using statement
using (var writer = new StreamWriter("journal.txt"))
{
writer.WriteLine("Day 1: Started learning C#");
writer.WriteLine("Day 2: Files are actually fun!");
writer.WriteLine("Day 3: Built my first app");
}
// File is automatically closed here, even if an error occurred
// Read with StreamWriter — line by line
Console.WriteLine("Journal entries:");
using (var reader = new StreamReader("journal.txt"))
{
string? line;
int lineNum = 1;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($" {lineNum}: {line}");
lineNum++;
}
}
// Modern C# shorthand: using declaration (no braces needed)
using var writer2 = new StreamWriter("notes.txt");
writer2.WriteLine("This using style disposes at end of scope");
writer2.WriteLine("Cleaner for simple cases!");
// writer2 is disposed when the enclosing scope ends
Console.WriteLine($"\nNotes: {File.ReadAllText("notes.txt")}");
Output
Journal entries:
  1: Day 1: Started learning C#
  2: Day 2: Files are actually fun!
  3: Day 3: Built my first app

Notes: This using style disposes at end of scope
Cleaner for simple cases!

Async File I/O — Don't Freeze Your App

File operations can be slow — especially on old hard drives or over networks. If you read a big file on the main thread, your app freezes while waiting. Nobody wants a frozen app.

Async methods (with async/await) let your program keep doing other things while the file is being read. All the File methods have async versions — just add Async to the method name and await the call.

Async File Operations & Practical Examples

// Async file operations
await File.WriteAllTextAsync("async-test.txt", "Written asynchronously!");
string asyncContent = await File.ReadAllTextAsync("async-test.txt");
Console.WriteLine(asyncContent);
// Practical example: simple config reader
var config = new Dictionary<string, string>();
string configData = "theme=dark\nlanguage=en\nfont_size=14\nauto_save=true";
await File.WriteAllTextAsync("config.ini", configData);
string[] configLines = await File.ReadAllLinesAsync("config.ini");
foreach (string line in configLines)
{
string[] parts = line.Split('=', 2);
if (parts.Length == 2)
config[parts[0].Trim()] = parts[1].Trim();
}
Console.WriteLine("\nConfig loaded:");
foreach (var pair in config)
Console.WriteLine($" {pair.Key} = {pair.Value}");
// Working with directories
string dir = "my_data";
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
await File.WriteAllTextAsync(Path.Combine(dir, "test.txt"), "Inside a folder!");
Console.WriteLine($"\nFiles in {dir}:");
foreach (string file in Directory.GetFiles(dir))
Console.WriteLine($" {Path.GetFileName(file)}");
Output
Written asynchronously!

Config loaded:
  theme = dark
  language = en
  font_size = 14
  auto_save = true

Files in my_data:
  test.txt
Note: 🔐 Always use 'using' with streams! If you forget, the file stays locked — other programs (and even your own code) can't access it until garbage collection eventually cleans up. The 'using' statement guarantees cleanup, even if an exception is thrown.

Quick check

What's the difference between File.WriteAllText and File.AppendAllText?
Exception HandlingDelegates, Events & Lambdas