System.IO is the .NET namespace for working with files, directories and data streams.
Any useful program sooner or later needs to leave RAM and “touch solid ground”: read a configuration file, save an error log, or process a list of images in a folder.
This namespace is immense, but today we’ll focus on file and directory management. And the first thing we need to understand is a fundamental design decision from Microsoft that confuses many people: the duality between Static Classes and Instance Classes.
Static or Instance Classes?
To handle files and folders, .NET gives us two class pairs that seem to do the same thing:
- Static:
FileandDirectory. - Instance:
FileInfoandDirectoryInfo.
Which one should I use? File.Exists(...) or new FileInfo(...).Exists?
These are “fire and forget” methods. Every time you call a method, the system must go to disk, check permissions and perform the action.
- Use: Ideal for one-off or occasional operations.
- Example: Check if a file exists a single time.
You create an object that represents the file in memory. This object holds file metadata and allows you to query it more conveniently.
- Use: Ideal if you’re going to perform multiple operations on the same file (read attributes, then date, then size). If the file changes externally, you can call
Refresh()to update that metadata.
General rule: If you’re doing just one thing, use File or Directory (you’ll write less code). If you’re going to loop or query multiple properties of the same element, use FileInfo or DirectoryInfo.
Managing Directories (Directory)
Let’s see how to create, delete and list folders.
Creating and Checking
The CreateDirectory method is idempotent. This means if the directory already exists, it does nothing and throws no error. It’s very safe.
string logsPath = Path.Combine(Directory.GetCurrentDirectory(), "logs");
if (!Directory.Exists(logsPath))
{
Directory.CreateDirectory(logsPath);
}
Listing Files Efficiently
We have two ways to get files from a folder, and the difference matters in large directories:
GetFiles(): Returns astring[].EnumerateFiles(): Returns anIEnumerable<string>.
If you have a folder with 100,000 photos:
GetFileswill wait until it finds all 100,000, load all names into memory (a giant array) and then return control to you. It’s slow and consumes a lot of RAM.EnumerateFilesreturns the first file as soon as it finds it, then the second… It allows you to start working immediately and uses very little memory.
// Better for large folders
foreach (string file in Directory.EnumerateFiles(logsPath, "*.txt"))
{
Console.WriteLine($"Processing: {file}");
}
Managing Files (File)
Basic operations are very straightforward:
string source = "document.txt";
string destination = "document_backup.txt";
if (File.Exists(source))
{
// Copy (overwrite = true allows overwriting if exists)
File.Copy(source, destination, true);
// Move (or rename)
File.Move(destination, "document_archived.txt");
// Delete
File.Delete(source);
}
Watch out for locking! System.IO is famous for throwing exceptions if you try to access a file that another program (or your own program) has open. Always make sure to close files or use using blocks.
Fast Reading and Writing
For simple operations (small text files), the File class has helper methods that open, read/write and close the file in a single line.
// Write text (overwrites everything)
string content = "Hello World from C#";
File.WriteAllText("note.txt", content);
// Append to the end
File.AppendAllText("note.txt", "\nNew line");
// Read everything (Careful with large files, loads everything into RAM)
string readText = File.ReadAllText("note.txt");
// Read line by line (Array)
string[] lines = File.ReadAllLines("note.txt");
Streams: The Foundation of Everything
The methods above (WriteAllText) are “syntactic sugar”. Under the hood, everything in System.IO works with Streams.
A Stream is like a pipe through which bytes flow. For large files, we must use Streams to avoid saturating memory.
To read text efficiently we use StreamReader and to write we use StreamWriter.
string path = "giant_book.txt";
// We use 'using' to ensure the file is closed and resources are released
using (StreamWriter sw = new StreamWriter(path))
{
sw.WriteLine("Chapter 1");
sw.WriteLine("In a village of La Mancha...");
}
using (StreamReader sr = new StreamReader(path))
{
string line;
// We read line by line without loading the entire book into memory
while ((line = sr.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
Async I/O
Disk access is slow. Extremely slow compared to the CPU.
If you read a large file on the main thread of a graphical application (WPF, Windows Forms) or in a Web API, your application will freeze until the reading finishes.
In applications with interfaces, APIs or concurrent services, it’s better to prefer the Async versions:
// Does not block the thread while waiting for the hard drive
string text = await File.ReadAllTextAsync("data.json");
// Write asynchronously
await File.WriteAllTextAsync("log.txt", "Operation completed");