Skip to main content

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");
Use File for small files

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);
}
}
Always use using with streams

Streams 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);
Always use Path.Combine

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:

ScenarioUse Static (File/Directory)Use Instance (FileInfo/DirectoryInfo)
Single operationYes — simplerOverhead of creating instance
Multiple operations on same pathRepeated lookupsCached metadata, more efficient
Need file metadata (size, times)Create FileInfo anywayNatural fit
Method chainingNot supportedSupported

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:

FileModeBehavior
CreateNewCreates new file; throws if exists
CreateCreates new file; overwrites if exists
OpenOpens existing file; throws if not exists
OpenOrCreateOpens if exists, creates if not
TruncateOpens and empties existing file
AppendOpens 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);
Use async for file I/O in async methods

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

ScenarioRecommended API
Small file read/writeFile.ReadAllText / WriteAllText
Large file processingStreamReader / StreamWriter
Multiple operations on same fileFileInfo instance
Directory traversalDirectory.EnumerateFiles (lazy)
Path manipulationPath.Combine, Path.GetFileName
Binary dataFileStream or File.ReadAllBytes
Async context (ASP.NET, UI)ReadAllTextAsync, StreamReader.ReadLineAsync
CSV or line-based parsingFile.ReadLines (lazy, memory-efficient)

Common Pitfalls

  1. Not using using with streams — Forgetting to dispose StreamReader/StreamWriter/FileStream leaves file handles open, causing locking issues and resource leaks.

  2. Loading large files into memoryFile.ReadAllText reads the entire file into a single string. For files over a few MB, use File.ReadLines (lazy IEnumerable) or StreamReader.ReadLineAsync to process line by line.

  3. Hardcoded path separators — Using "folder\\file.txt" breaks on Linux/macOS. Always use Path.Combine or Path.DirectorySeparatorChar.

  4. Swallowing IOException — File operations can fail for many reasons (permissions, disk full, file in use). Catch IOException specifically and handle meaningfully rather than catching all exceptions.

  5. Race conditions with File.Exists — Checking File.Exists before reading/writing is unreliable — the file state can change between the check and the operation. Handle FileNotFoundException instead.

  6. Not handling file locks — Another process may have a file open. Use FileStream with FileShare to control sharing, or retry with backoff for locked files.

Key Takeaways

  1. Use File/Directory static classes for simple, one-shot operations on small files.
  2. Use StreamReader/StreamWriter for large files or incremental reading/writing.
  3. Always wrap streams in using to ensure file handles are released.
  4. Use Path.Combine for cross-platform path construction.
  5. Prefer async methods (ReadAllTextAsync, ReadLineAsync) in ASP.NET and UI applications.
  6. Use FileInfo/DirectoryInfo when performing multiple operations on the same path.
  7. File.ReadLines returns a lazy IEnumerable<string> — use it instead of ReadAllLines for large files.

Interview Questions

Q: What is the difference between File and FileInfo?

File is a static class where each method performs a security check and file lookup. FileInfo is an instance class that caches file metadata, making it more efficient for multiple operations on the same file. Use File for single operations; FileInfo for repeated access.

Q: What is the difference between StreamReader and FileStream?

StreamReader reads text (characters) with automatic encoding handling — it wraps a FileStream internally. FileStream reads raw bytes and gives you low-level control over file access mode, sharing, and seeking. Use StreamReader for text files; FileStream for 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 calls Dispose) closes the handle immediately, preventing resource leaks and file locking issues.

Q: What is the difference between File.ReadLines and File.ReadAllLines?

ReadAllLines loads the entire file into a string[] array in memory. ReadLines returns a lazy IEnumerable<string> that reads lines one at a time. For large files, ReadLines is 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, and StreamWriter.WriteLineAsync. These use await to release the thread while I/O completes, improving scalability in ASP.NET and responsiveness in UI applications.

References