C# File I/O Operations: Practical Examples and Best Practices
Introduction to C# File I/O Operations
C# File I/O (Input/Output) operations give your applications the ability to:
- Read data from files (configurations, data imports, templates)
- Write data to files (logs, reports, exports)
- Persist complex objects between runs (user profiles, settings, cached data)
All of this is primarily done through the System.IO namespace and related APIs. In .NET, file operations are synchronous (blocking) or asynchronous (non-blocking), and can work with both text and binary data.
In this article, you’ll learn through 6 practical examples:
- Reading text from a file (line by line and all at once)
- Writing and appending text to a file
- Reading and writing binary data
- Serializing and deserializing objects with JSON (modern replacement for
BinaryFormatter) - Safely checking for file existence and handling common exceptions
- Using asynchronous File I/O for responsive apps
Along the way, we’ll highlight important notes, pro tips, and common pitfalls.
Important Note
File I/O is relatively slow compared with in-memory operations. According to general performance guidance from Microsoft, disk access is one of the slowest operations in typical applications, so you should batch reads/writes when possible and avoid unnecessary disk access.
1. Reading Text from a File
Reading from text files is a core task in many applications: config files, CSV imports, templates, and more.
1.1 Basic Example: Read All Lines from a File
Use Case: Small configuration or data files where file size is limited (for example, less than a few MB).
using System;
using System.IO;
class Program
{
static void Main()
{
string filePath = "config.txt";
try
{
// Reads the entire file into memory as an array of strings (one per line)
string[] lines = File.ReadAllLines(filePath);
Console.WriteLine("Configuration file contents:\n");
foreach (string line in lines)
{
Console.WriteLine(line);
}
}
catch (FileNotFoundException)
{
Console.WriteLine($"File not found: {filePath}");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("You do not have permission to read this file.");
}
catch (IOException ex)
{
Console.WriteLine("An I/O error occurred: " + ex.Message);
}
}
}
When to Use File.ReadAllLines
- The file is small to medium-sized.
- You want simple code and can afford loading the whole file into memory.
Pro Tip
For very large files, avoidFile.ReadAllLinesbecause it loads everything into memory. Instead, read the file line by line usingFile.ReadLinesorStreamReader.
1.2 Memory-Friendly Example: Read File Line by Line
Use Case: Large log files, data dumps, or any file that might grow beyond a few MB.
using System;
using System.IO;
class Program
{
static void Main()
{
string filePath = "large-log.txt";
try
{
// File.ReadLines uses deferred execution and streams the file
foreach (string line in File.ReadLines(filePath))
{
// Process each line as you go
if (line.Contains("ERROR"))
{
Console.WriteLine("Found error: " + line);
}
}
}
catch (Exception ex) when (ex is FileNotFoundException ||
ex is UnauthorizedAccessException ||
ex is IOException)
{
Console.WriteLine("Could not read the log file: " + ex.Message);
}
}
}
Benefits of streaming (File.ReadLines):
- Uses less memory
- Starts processing immediately, without waiting to read the whole file
2. Writing and Appending Text to a File
Writing to files is common for logging, exporting reports, or generating simple data files.
2.1 Writing Text to a New File (Overwrite)
Use Case: Generating a report or exporting data where each run should create a fresh file.
using System;
using System.IO;
class Program
{
static void Main()
{
string filePath = "report.txt";
string[] reportLines =
{
"Sales Report",
"============",
$"Generated at: {DateTime.Now}",
"",
"Total Orders: 150",
"Total Revenue: $25,000"
};
try
{
// This will create the file or overwrite it if it already exists
File.WriteAllLines(filePath, reportLines);
Console.WriteLine("Report generated successfully.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("You do not have permission to write to this location.");
}
catch (IOException ex)
{
Console.WriteLine("An I/O error occurred: " + ex.Message);
}
}
}
2.2 Appending Text to a File (Logging)
Use Case: Logging application events, errors, or audit trails.
using System;
using System.IO;
class Program
{
static void Main()
{
string filePath = "application.log";
string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Application started";
try
{
// Append text to the file; create it if it doesn't exist
using (StreamWriter writer = new StreamWriter(filePath, append: true))
{
writer.WriteLine(logEntry);
}
Console.WriteLine("Log entry written.");
}
catch (IOException ex)
{
Console.WriteLine("Failed to write log: " + ex.Message);
}
}
}
Important Note
Theappend: trueparameter tellsStreamWriterto add to the existing file instead of overwriting it. This is crucial for logs.
Logging Best Practices (High Level)
- Include timestamps in each log entry.
- Use consistent formats (e.g., ISO 8601 for dates).
- Consider using a dedicated logging framework like
SerilogorNLogfor production systems.
3. Reading and Writing Binary Data
Not all files are text. Images, PDFs, and many custom formats are binary. For these, you work with byte arrays.
3.1 Copying a Binary File
Use Case: Backing up or duplicating files such as images or documents.
using System;
using System.IO;
class Program
{
static void Main()
{
string sourcePath = "photo.jpg";
string destinationPath = "photo-backup.jpg";
try
{
byte[] bytes = File.ReadAllBytes(sourcePath);
File.WriteAllBytes(destinationPath, bytes);
Console.WriteLine("File copied successfully.");
}
catch (FileNotFoundException)
{
Console.WriteLine("Source file not found.");
}
catch (IOException ex)
{
Console.WriteLine("An I/O error occurred: " + ex.Message);
}
}
}
3.2 Streaming Binary Data for Large Files
For large files (videos, large archives), reading everything into memory is inefficient. Instead, stream with FileStream.
using System;
using System.IO;
class Program
{
static void Main()
{
string sourcePath = "large-video.mp4";
string destinationPath = "large-video-copy.mp4";
const int bufferSize = 81920; // 80 KB buffer (common default)
try
{
using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
using (FileStream destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write))
{
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
destinationStream.Write(buffer, 0, bytesRead);
}
}
Console.WriteLine("Large file copied using streaming.");
}
catch (Exception ex)
{
Console.WriteLine("Error copying file: " + ex.Message);
}
}
}
Pro Tip
Streaming is critical when working with very large files or on systems with limited memory. It also allows you to show progress indicators while copying.
4. Serializing and Deserializing Objects (Modern JSON Approach)
The original example used BinaryFormatter, which is now considered obsolete and insecure for many scenarios. Microsoft strongly recommends using safer, modern serializers such as System.Text.Json or Newtonsoft.Json.
For most applications, JSON is a great choice:
- Human-readable
- Widely supported across languages
- Safer than
BinaryFormatterfor untrusted data
4.1 Example: Saving and Loading a User Profile with JSON
Use Case: Persisting user settings or application state between sessions.
using System;
using System.IO;
using System.Text.Json;
class User
{
public string Name { get; set; }
public int Age { get; set; }
public bool IsAdmin { get; set; }
}
class Program
{
static void Main()
{
string filePath = "user.json";
var user = new User
{
Name = "John Doe",
Age = 30,
IsAdmin = true
};
// Serialize to JSON and save to file
try
{
var options = new JsonSerializerOptions
{
WriteIndented = true // Pretty-print JSON
};
string json = JsonSerializer.Serialize(user, options);
File.WriteAllText(filePath, json);
Console.WriteLine("User serialized to JSON successfully.");
}
catch (IOException ex)
{
Console.WriteLine("Error writing JSON file: " + ex.Message);
}
// Read JSON from file and deserialize
try
{
string jsonFromFile = File.ReadAllText(filePath);
User? loadedUser = JsonSerializer.Deserialize<User>(jsonFromFile);
if (loadedUser != null)
{
Console.WriteLine($"Loaded user: {loadedUser.Name}, Age: {loadedUser.Age}, Admin: {loadedUser.IsAdmin}");
}
}
catch (JsonException ex)
{
Console.WriteLine("Invalid JSON format: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("Error reading JSON file: " + ex.Message);
}
}
}
Important Note
BinaryFormatteris deprecated due to security risks when handling untrusted data. For details, see Microsoft’s guidance on serialization security in .NET: Microsoft Docs - BinaryFormatter security guide.
5. Safely Checking for File Existence and Handling Errors
Robust file handling means anticipating failures: missing files, permission issues, locked files, and invalid paths.
5.1 Checking if a File Exists
using System;
using System.IO;
class Program
{
static void Main()
{
string filePath = "data.csv";
if (File.Exists(filePath))
{
Console.WriteLine($"File '{filePath}' exists. Proceeding to read...");
// Safe to attempt reading here
}
else
{
Console.WriteLine($"File '{filePath}' does not exist. Creating a new one...");
File.WriteAllText(filePath, "Id,Name\n");
}
}
}
Pro Tip
Between checkingFile.Existsand actually opening the file, the file’s state can change (this is called a race condition). Always keep exception handling around the actual file operations even if you checked beforehand.
5.2 Common Exceptions to Handle
When working with files, you’ll frequently see:
FileNotFoundException– The file you tried to open doesn’t exist.DirectoryNotFoundException– The path is invalid or a directory in the path doesn’t exist.UnauthorizedAccessException– No permission to access the file or folder.IOException– General I/O error (file locked by another process, disk full, etc.).
A simple pattern is to group these in a try/catch block and report user-friendly messages.
6. Asynchronous File I/O (Async/Await)
Asynchronous file operations help keep your UI responsive (in desktop/mobile apps) or improve scalability (in web apps). Instead of blocking a thread while waiting for disk I/O, async methods free the thread to handle other work.
6.1 Example: Asynchronously Writing a Large Log File
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string filePath = "async-log.txt";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.AppendLine($"[{DateTime.Now:O}] Log entry #{i}");
}
try
{
await File.WriteAllTextAsync(filePath, sb.ToString());
Console.WriteLine("Asynchronous log file written.");
}
catch (IOException ex)
{
Console.WriteLine("Error writing file asynchronously: " + ex.Message);
}
}
}
6.2 Example: Asynchronously Reading a Configuration File
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string filePath = "settings.json";
try
{
if (!File.Exists(filePath))
{
Console.WriteLine("Settings file not found. Using defaults.");
return;
}
string json = await File.ReadAllTextAsync(filePath);
Console.WriteLine("Settings file contents:\n" + json);
}
catch (IOException ex)
{
Console.WriteLine("Error reading file asynchronously: " + ex.Message);
}
}
}
Pro Tip
Use async I/O in:
- ASP.NET Core web apps (to improve throughput)
- GUI apps (WinForms, WPF, MAUI) to keep the UI responsive
- Any scenario where blocking threads on I/O would hurt performance
For more background on asynchronous programming patterns, see Microsoft’s async/await documentation.
7. Practical Tips and Best Practices for C# File I/O
Here are some overarching guidelines to keep your file I/O code robust and maintainable:
Use
usingstatements orawait using
Always ensure streams and writers are disposed properly. This releases file handles and prevents file locks.Prefer JSON or XML for configuration and state
- Human-readable
- Easier to debug
- Supported by many tools and platforms
Validate user input when building file paths
- Avoid directly concatenating user input into file paths.
- Consider using
Path.Combineand validating allowed directories.
Be aware of cross-platform paths
.NET runs on Windows, Linux, and macOS. UsePath.DirectorySeparatorCharandPath.Combineinstead of hard-coding"\\"or"/".Handle encoding explicitly when needed
- Default encoding can vary.
- For consistent behavior, specify
Encoding.UTF8or another explicit encoding.
Consider concurrency
- If multiple processes or threads write to the same file, you may need file locks or a logging framework designed for concurrency.
For deeper reading on file system APIs and best practices, the official .NET documentation is an excellent resource: Microsoft Docs - File and Stream I/O.
Frequently Asked Questions (FAQ)
1. What is the difference between File.ReadAllText and File.ReadAllLines?
File.ReadAllText(path)returns the entire file as a single string.File.ReadAllLines(path)returns the file as a string array, with one entry per line.
Use ReadAllText when you want to process the file as a block of text, and ReadAllLines when you want to iterate over lines easily.
2. When should I use asynchronous file I/O in C#?
Use async I/O (ReadAllTextAsync, WriteAllTextAsync, etc.) when blocking a thread would be costly:
- In web applications (ASP.NET Core) to improve scalability.
- In desktop or mobile apps to keep the UI responsive.
- In services or background tasks that perform heavy or frequent disk operations.
For small, infrequent operations in console utilities, synchronous I/O is often sufficient.
3. Is BinaryFormatter still safe to use for serialization?
BinaryFormatter is not recommended for new development. Microsoft has marked it as obsolete for security reasons, especially when handling untrusted data. Instead, use safer serializers such as:
System.Text.Json(built into .NET)XmlSerializer(for XML)Newtonsoft.Json(popular third-party JSON library)
See Microsoft’s guidance on serialization security for more details: BinaryFormatter security guide.
4. How large can a file be before I should avoid ReadAllText or ReadAllLines?
There is no strict limit, but as a rule of thumb:
- For files under a few megabytes,
ReadAllTextandReadAllLinesare usually fine. - For files that can grow large (hundreds of MB or more), prefer streaming with
File.ReadLinesorStreamReaderto avoid high memory usage and potentialOutOfMemoryException.
5. How do I handle special characters or different languages in text files?
Use a consistent character encoding, such as UTF-8:
using System.IO;
using System.Text;
File.WriteAllText("data.txt", "Café – こんにちは", Encoding.UTF8);
string content = File.ReadAllText("data.txt", Encoding.UTF8);
UTF-8 is widely used and supports most languages and symbols. For more on character encodings, see Unicode on Wikipedia.
Related Topics
Practical examples of polymorphism in C++: function overloading
Best examples of dynamic memory allocation in C++: new and delete
Practical examples of C# variable declaration and initialization examples
Modern examples of C++ operator overloading: custom operators examples that actually matter
Best examples of C# conditional statements: examples & explanations
Real‑world examples of C# exception handling: 3 practical patterns every developer should know
Explore More C++ Code Snippets
Discover more examples and insights in this category.
View All C++ Code Snippets