C# File I/O Operations: Practical Examples and Best Practices

File I/O (Input/Output) is one of the most common tasks you’ll perform in real-world C# applications. Whether you’re reading configuration files, writing logs, or saving user data, understanding how to work with files safely and efficiently is essential. In this guide, you’ll walk through practical, modern examples of C# file operations using the .NET `System.IO` APIs and related libraries. We’ll start with simple tasks like reading and writing text files, then move on to more advanced scenarios such as appending logs, working with binary data, and serializing objects to JSON. Along the way, you’ll see how to handle errors gracefully, avoid common performance pitfalls, and follow best practices for security and maintainability. By the end, you’ll have a solid set of reusable patterns you can drop into your own projects, plus a clearer mental model of how C# interacts with the file system on Windows, Linux, and macOS.
Written by
Taylor

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:

  1. Reading text from a file (line by line and all at once)
  2. Writing and appending text to a file
  3. Reading and writing binary data
  4. Serializing and deserializing objects with JSON (modern replacement for BinaryFormatter)
  5. Safely checking for file existence and handling common exceptions
  6. 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, avoid File.ReadAllLines because it loads everything into memory. Instead, read the file line by line using File.ReadLines or StreamReader.

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
The append: true parameter tells StreamWriter to 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 Serilog or NLog for 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 BinaryFormatter for 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
BinaryFormatter is 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 checking File.Exists and 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:

  1. Use using statements or await using
    Always ensure streams and writers are disposed properly. This releases file handles and prevents file locks.

  2. Prefer JSON or XML for configuration and state

    • Human-readable
    • Easier to debug
    • Supported by many tools and platforms
  3. Validate user input when building file paths

    • Avoid directly concatenating user input into file paths.
    • Consider using Path.Combine and validating allowed directories.
  4. Be aware of cross-platform paths
    .NET runs on Windows, Linux, and macOS. Use Path.DirectorySeparatorChar and Path.Combine instead of hard-coding "\\" or "/".

  5. Handle encoding explicitly when needed

    • Default encoding can vary.
    • For consistent behavior, specify Encoding.UTF8 or another explicit encoding.
  6. 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, ReadAllText and ReadAllLines are usually fine.
  • For files that can grow large (hundreds of MB or more), prefer streaming with File.ReadLines or StreamReader to avoid high memory usage and potential OutOfMemoryException.

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.

Explore More C++ Code Snippets

Discover more examples and insights in this category.

View All C++ Code Snippets