Skip to content

C# - File I/O

File Input/Output (I/O) is a fundamental part of most applications. It involves reading data from and writing data to files on a storage device. In .NET, these operations are primarily managed by classes within the System.IO namespace.

When a file is opened, it’s represented as a stream, which is a sequence of bytes. An input stream reads data from a source (like a file), and an output stream writes data to a destination.

The System.IO namespace provides a rich set of tools for file and directory manipulation. Here are some of the most important classes for modern development:

ClassPrimary Use Case
FileProvides static methods for quick and simple file operations like reading all text, writing all lines, copying, and deleting.
DirectoryProvides static methods for creating, moving, and enumerating through directories and subdirectories.
PathProvides methods for processing directory and file path strings in a cross-platform way (e.g., Path.Combine).
StreamReaderReads characters from a byte stream, ideal for reading text files line-by-line.
StreamWriterWrites characters to a stream, ideal for writing text files.
FileStreamA low-level class providing a stream for a file, supporting both synchronous and asynchronous read/write operations with fine-grained control.

Essential Best Practice: Resource Management with using

Section titled “Essential Best Practice: Resource Management with using”

File handles are unmanaged resources that must be properly closed to prevent memory leaks and file locking issues. The using statement (or the newer using declaration) is the standard and safest way to ensure that IDisposable objects like streams are automatically closed and disposed of, even if an exception occurs.

// C# 8.0 and later 'using' declaration
// The 'writer' is disposed at the end of the current scope (the Main method).
using var writer = new StreamWriter("log.txt");
writer.WriteLine("Log entry.");
// Traditional 'using' statement
// The 'reader' is disposed at the end of the using block.
using (var reader = new StreamReader("log.txt"))
{
string? line = reader.ReadLine();
Console.WriteLine(line);
}

Simple File Operations with the File Class

Section titled “Simple File Operations with the File Class”

For many common tasks, the static methods on the File class are the easiest and most efficient approach.

// Ensure cross-platform path safety with Path.Combine
string filePath = Path.Combine(Directory.GetCurrentDirectory(), "mydata.txt");
// 1. Write content to a file (creates or overwrites)
string[] content = { "First line", "Second line", "Third line" };
File.WriteAllLines(filePath, content);
Console.WriteLine("File created and content written.");
// 2. Append content to an existing file
File.AppendAllText(filePath, "This is an appended line.\n");
Console.WriteLine("Content appended.");
// 3. Read all content from a file
string fileContent = File.ReadAllText(filePath);
Console.WriteLine("\n--- File Content ---");
Console.WriteLine(fileContent);
// 4. Check if a file exists and delete it
if (File.Exists(filePath))
{
File.Delete(filePath);
Console.WriteLine("\nFile deleted successfully.");
}

Asynchronous File I/O with async and await

Section titled “Asynchronous File I/O with async and await”

In modern applications (especially UI, web, and server applications), it’s crucial to perform I/O operations asynchronously to avoid blocking threads. This improves scalability and responsiveness. The async and await keywords make this straightforward.

// This method must be called from an async context, like 'await ReadFileAsync()'.
async Task ReadFileAsync(string filePath)
{
try
{
// Create a dummy file for the example
await File.WriteAllTextAsync(filePath, "This is an async example.");
Console.WriteLine("Reading file asynchronously...");
string content = await File.ReadAllTextAsync(filePath);
Console.WriteLine($"File content: {content}");
// Clean up the dummy file
File.Delete(filePath);
}
catch (IOException ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
// To run this, you would need an async Main method:
// static async Task Main(string[] args) { ... }
await ReadFileAsync("async_test.txt");

While the File class is great for simple operations, FileStream gives you fine-grained control over file access, such as specifying file modes, access rights, and sharing options. It’s often used for working with binary files or very large files that you want to process in chunks.

string binaryFilePath = "test.dat";
// Use 'FileMode.Create' to create a new file or overwrite an existing one.
using (FileStream fs = new FileStream(binaryFilePath, FileMode.Create, FileAccess.Write))
{
// BinaryWriter makes it easy to write primitive types.
using (var writer = new BinaryWriter(fs))
{
writer.Write(123); // int
writer.Write(3.14159); // double
writer.Write("Hello, Binary!"); // string
}
}
// Use 'FileMode.Open' to read from an existing file.
using (FileStream fs = new FileStream(binaryFilePath, FileMode.Open, FileAccess.Read))
{
// BinaryReader reads the data back in the same order.
using (var reader = new BinaryReader(fs))
{
int myInt = reader.ReadInt32();
double myDouble = reader.ReadDouble();
string myString = reader.ReadString();
Console.WriteLine($"Read Integer: {myInt}");
Console.WriteLine($"Read Double: {myDouble}");
Console.WriteLine($"Read String: {myString}");
}
}
  • Path Traversal: Never construct file paths by concatenating user input directly. A malicious user could provide input like ../../secrets.txt. Always use Path.Combine() which correctly handles path separators, and validate user input.
  • Error Handling: File operations can fail for many reasons (FileNotFoundException, UnauthorizedAccessException, etc.). Always wrap I/O code in try-catch blocks to handle potential IOException and its derived classes gracefully.
  • Working with JSON/XML: A very common use case is serializing objects to JSON or XML files. Libraries like System.Text.Json (for JSON) and System.Xml.Serialization integrate seamlessly with file streams for this purpose.
  • Large Files: When working with very large files, avoid methods like File.ReadAllBytes() which load the entire file into memory. Instead, use a FileStream to read the file in smaller, manageable chunks.