File I/O
Definition
File I/O (Input/Output) in C# refers to reading from and writing to files and directories on the file system. .NET provides multiple layers of abstraction: low-level stream classes (FileStream, StreamReader, StreamWriter), high-level static helpers (File, Directory), and informative wrapper classes (FileInfo, DirectoryInfo). All file I/O APIs live in the System.IO namespace.
using System.IO;
// High-level — one-liners for simple operations
string text = File.ReadAllText("data.txt");
File.WriteAllText("output.txt", "Hello, World!");
// Stream-based — fine-grained control for large files
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine("Application started");
Core Concepts
File Class
System.IO.File is a static class providing one-shot methods for common file operations. Each method opens and closes the file internally — ideal for small files.
// Reading
string text = File.ReadAllText("data.txt"); // Entire file as string
string[] lines = File.ReadAllLines("data.txt"); // Line-by-line array
byte[] bytes = File.ReadAllBytes("image.png"); // Raw bytes
// Writing
File.WriteAllText("output.txt", "Hello, World!"); // Overwrite with string
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" }); // Overwrite with lines
File.WriteAllBytes("binary.dat", new byte[] { 0x01, 0x02 }); // Write raw bytes
// Appending
File.AppendAllText("log.txt", "New entry\n");
File.AppendAllLines("log.txt", new[] { "Entry 1", "Entry 2" });
// File information
bool exists = File.Exists("data.txt");
DateTime modified = File.GetLastWriteTime("data.txt");
long size = new FileInfo("data.txt").Length;
// Copy, Move, Delete
File.Copy("source.txt", "dest.txt", overwrite: true);
File.Move("old.txt", "new.txt");
File.Delete("temp.txt");
File.ReadAllText / WriteAllText are perfect for small files. For large files, use StreamReader/StreamWriter to avoid loading the entire content into memory at once.
StreamReader and StreamWriter
Stream-based classes for line-by-line or character-level reading and writing. They handle encoding automatically and are ideal for large or ongoing file operations.
StreamWriter
// Write to file (overwrites by default)
using (var writer = new StreamWriter("output.txt"))
{
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Write("No newline at end");
}
// Append to existing file
using (var writer = new StreamWriter("log.txt", append: true))
{
writer.WriteLine($"[{DateTime.UtcNow:O}] Application started");
}
// With specific encoding
using var writer = new StreamWriter("output.txt", append: false, Encoding.UTF8);
writer.WriteLine("Unicode content: café, naïve");
StreamReader
// Read entire file
using (var reader = new StreamReader("data.txt"))
{
string content = reader.ReadToEnd();
}
// Read line by line
using (var reader = new StreamReader("data.txt"))
{
string? line;
while ((line = reader.ReadLine()) is not null)
{
Console.WriteLine(line);
}
}
// Read character by character
using (var reader = new StreamReader("data.txt"))
{
int ch;
while ((ch = reader.Read()) != -1)
{
Console.Write((char)ch);
}
}
using with streamsStreams wrap unmanaged file handles. The using statement (or using declaration) ensures the handle is released immediately when you are done, even if an exception occurs. Without it, the handle stays open until the garbage collector runs, which can cause file locking issues.
Directory Class
System.IO.Directory is a static class for directory operations:
// Create
Directory.CreateDirectory("output/logs"); // Creates all intermediate directories
// Check existence
bool exists = Directory.Exists("output");
// List contents
string[] files = Directory.GetFiles("output"); // All files
string[] dirs = Directory.GetDirectories("output"); // All subdirectories
string[] allFiles = Directory.GetFiles("output", "*.txt", SearchOption.AllDirectories); // Recursive
// Enumerate — lazy version (better for large directories)
IEnumerable<string> largeFiles = Directory.EnumerateFiles("output")
.Where(f => new FileInfo(f).Length > 1_000_000);
// Move, Delete
Directory.Move("oldPath", "newPath");
Directory.Delete("temp", recursive: true); // Delete directory and all contents
// Common paths
string cwd = Directory.GetCurrentDirectory();
string[] logicalDrives = Directory.GetLogicalDrives();
Path Class
System.IO.Path is a static class for cross-platform path manipulation. It handles path separators (\ on Windows, / on Linux/macOS) automatically:
// Combining paths
string fullPath = Path.Combine("folder", "subfolder", "file.txt"); // folder/subfolder/file.txt
// Extracting parts
Path.GetDirectoryName("folder/subfolder/file.txt"); // folder/subfolder
Path.GetFileName("folder/subfolder/file.txt"); // file.txt
Path.GetFileNameWithoutExtension("data.txt"); // data
Path.GetExtension("data.txt"); // .txt
Path.ChangeExtension("data.txt", ".csv"); // data.csv
// Temporary files
string tempFile = Path.GetTempFileName(); // Creates empty temp file, returns path
string tempDir = Path.GetTempPath(); // Temp directory path
// Special paths
string docs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
Never concatenate paths with + or string interpolation. Path.Combine handles trailing slashes and cross-platform separators correctly. "folder" + "\\" + "file.txt" breaks on Linux; Path.Combine("folder", "file.txt") does not.
FileInfo and DirectoryInfo
Unlike the static File/Directory classes, FileInfo and DirectoryInfo are instance classes. Create them when you need multiple operations on the same file or directory — they cache file system information to avoid repeated lookups.
// FileInfo — instance-based file operations
var file = new FileInfo("data.txt");
Console.WriteLine($"Name: {file.Name}"); // data.txt
Console.WriteLine($"Extension: {file.Extension}"); // .txt
Console.WriteLine($"Size: {file.Length} bytes"); // File size
Console.WriteLine($"Created: {file.CreationTime}"); // Creation time
Console.WriteLine($"Modified: {file.LastWriteTime}"); // Last modified
Console.WriteLine($"Directory: {file.DirectoryName}"); // Parent directory
file.CopyTo("backup.txt", overwrite: true);
file.MoveTo("archive/data.txt");
file.Delete();
// DirectoryInfo — instance-based directory operations
var dir = new DirectoryInfo("output");
Console.WriteLine($"Name: {dir.Name}"); // output
Console.WriteLine($"Parent: {dir.Parent}"); // Parent directory
Console.WriteLine($"Root: {dir.Root}"); // Root drive (e.g., C:\)
FileInfo[] allFiles = dir.GetFiles("*", SearchOption.AllDirectories);
DirectoryInfo[] subDirs = dir.GetDirectories();
dir.Create(); // Create if not exists
dir.CreateSubdirectory("logs"); // Create subdirectory
dir.Delete(recursive: true); // Delete with contents
Static class vs instance class:
| Scenario | Use Static (File/Directory) | Use Instance (FileInfo/DirectoryInfo) |
|---|---|---|
| Single operation | Yes — simpler | Overhead of creating instance |
| Multiple operations on same path | Repeated lookups | Cached metadata, more efficient |
| Need file metadata (size, times) | Create FileInfo anyway | Natural fit |
| Method chaining | Not supported | Supported |
FileStream
Low-level stream for byte-oriented file access with explicit control over buffering, seeking, and file sharing:
// Read and write with FileStream
using var fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write);
byte[] data = Encoding.UTF8.GetBytes("Hello, binary world!");
fs.Write(data, 0, data.Length);
// Read with buffering
using var fs2 = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
var buffer = new byte[1024];
int bytesRead = fs2.Read(buffer, 0, buffer.Length);
string result = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// Seek to specific position
fs2.Seek(0, SeekOrigin.Begin); // Rewind to start
FileMode options:
| FileMode | Behavior |
|---|---|
CreateNew | Creates new file; throws if exists |
Create | Creates new file; overwrites if exists |
Open | Opens existing file; throws if not exists |
OpenOrCreate | Opens if exists, creates if not |
Truncate | Opens and empties existing file |
Append | Opens or creates; seeks to end |
Async File Operations
File I/O can be slow, especially with network drives or large files. Async methods prevent thread blocking:
// Async StreamReader
public async Task<string> ReadFileAsync(string path)
{
using var reader = new StreamReader(path);
return await reader.ReadToEndAsync();
}
// Async StreamReader — line by line
public async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
yield return line;
}
}
// Async StreamWriter
public async Task WriteLogAsync(string path, string message)
{
await using var writer = new StreamWriter(path, append: true);
await writer.WriteLineAsync($"[{DateTime.UtcNow:O}] {message}");
}
// File class async methods (C# 6+)
string text = await File.ReadAllTextAsync("data.txt");
await File.WriteAllTextAsync("output.txt", "Hello, async!");
await File.AppendAllTextAsync("log.txt", "New entry\n");
string[] lines = await File.ReadAllLinesAsync("data.txt");
await File.WriteAllLinesAsync("output.txt", lines);
In ASP.NET or UI applications, always use ReadAllTextAsync / WriteAllTextAsync / StreamReader.ReadLineAsync instead of their synchronous counterparts. Synchronous file I/O blocks the thread, reducing scalability.
Common Patterns
Reading CSV-like data
var records = new List<(string Name, int Age, string City)>();
foreach (string line in File.ReadLines("people.csv").Skip(1)) // Skip header
{
string[] parts = line.Split(',');
records.Add((parts[0], int.Parse(parts[1]), parts[2]));
}
Logging to file
public class FileLogger
{
private readonly string _logPath;
private readonly Lock _lock = new(); // .NET 9+ Lock; use object for older versions
public FileLogger(string logPath) => _logPath = logPath;
public void Log(string message)
{
lock (_lock)
{
File.AppendAllText(_logPath, $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] {message}\n");
}
}
public async Task LogAsync(string message)
{
await File.AppendAllTextAsync(
_logPath,
$"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] {message}\n");
}
}
Temporary file handling
// Create a temp file — automatically named, in temp directory
string tempFile = Path.GetTempFileName();
try
{
File.WriteAllText(tempFile, "Temporary data");
// Process...
}
finally
{
if (File.Exists(tempFile))
File.Delete(tempFile);
}
When to Use
| Scenario | Recommended API |
|---|---|
| Small file read/write | File.ReadAllText / WriteAllText |
| Large file processing | StreamReader / StreamWriter |
| Multiple operations on same file | FileInfo instance |
| Directory traversal | Directory.EnumerateFiles (lazy) |
| Path manipulation | Path.Combine, Path.GetFileName |
| Binary data | FileStream or File.ReadAllBytes |
| Async context (ASP.NET, UI) | ReadAllTextAsync, StreamReader.ReadLineAsync |
| CSV or line-based parsing | File.ReadLines (lazy, memory-efficient) |
Common Pitfalls
-
Not using
usingwith streams — Forgetting to disposeStreamReader/StreamWriter/FileStreamleaves file handles open, causing locking issues and resource leaks. -
Loading large files into memory —
File.ReadAllTextreads the entire file into a single string. For files over a few MB, useFile.ReadLines(lazyIEnumerable) orStreamReader.ReadLineAsyncto process line by line. -
Hardcoded path separators — Using
"folder\\file.txt"breaks on Linux/macOS. Always usePath.CombineorPath.DirectorySeparatorChar. -
Swallowing IOException — File operations can fail for many reasons (permissions, disk full, file in use). Catch
IOExceptionspecifically and handle meaningfully rather than catching all exceptions. -
Race conditions with File.Exists — Checking
File.Existsbefore reading/writing is unreliable — the file state can change between the check and the operation. HandleFileNotFoundExceptioninstead. -
Not handling file locks — Another process may have a file open. Use
FileStreamwithFileShareto control sharing, or retry with backoff for locked files.
Key Takeaways
- Use
File/Directorystatic classes for simple, one-shot operations on small files. - Use
StreamReader/StreamWriterfor large files or incremental reading/writing. - Always wrap streams in
usingto ensure file handles are released. - Use
Path.Combinefor cross-platform path construction. - Prefer async methods (
ReadAllTextAsync,ReadLineAsync) in ASP.NET and UI applications. - Use
FileInfo/DirectoryInfowhen performing multiple operations on the same path. File.ReadLinesreturns a lazyIEnumerable<string>— use it instead ofReadAllLinesfor large files.
Interview Questions
Q: What is the difference between File and FileInfo?
Fileis a static class where each method performs a security check and file lookup.FileInfois an instance class that caches file metadata, making it more efficient for multiple operations on the same file. UseFilefor single operations;FileInfofor repeated access.
Q: What is the difference between StreamReader and FileStream?
StreamReaderreads text (characters) with automatic encoding handling — it wraps aFileStreaminternally.FileStreamreads raw bytes and gives you low-level control over file access mode, sharing, and seeking. UseStreamReaderfor text files;FileStreamfor binary data.
Q: Why should you use using with streams?
Streams hold unmanaged file handles that are not released by the garbage collector in a timely manner.
using(which callsDispose) closes the handle immediately, preventing resource leaks and file locking issues.
Q: What is the difference between File.ReadLines and File.ReadAllLines?
ReadAllLinesloads the entire file into astring[]array in memory.ReadLinesreturns a lazyIEnumerable<string>that reads lines one at a time. For large files,ReadLinesis significantly more memory-efficient.
Q: How do you handle file I/O asynchronously in C#?
Use async methods like
File.ReadAllTextAsync,File.WriteAllTextAsync,StreamReader.ReadLineAsync, andStreamWriter.WriteLineAsync. These useawaitto release the thread while I/O completes, improving scalability in ASP.NET and responsiveness in UI applications.