Skip to Content
Course content

66: Reading and Writing Files with System.IO

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact scenario play out in code reviews more times than I can count. A developer is building a simple logging system, everything works perfectly during the first test run, and then suddenly, the application crashes with a IOException claiming the file is being used by another process. Here is the kind of code that causes that headache:

public void LogEvent(string message)
{
    // We want to append the message to the log file
    StreamWriter writer = new StreamWriter("log.txt", true);
    writer.WriteLine($"{DateTime.Now}: {message}");
    // Oops, we forgot to close the writer!
}

The "File in Use" Nightmare

At first glance, this looks fine. You open the file, write the line, and the method ends. But here is the catch: the StreamWriter holds a "lock" on that file. Because we never called writer.Close() or writer.Dispose(), the operating system thinks the program is still actively writing to log.txt. If you call LogEvent a second time immediately after the first, the second call tries to open the same file, finds the lock still active from the first call, and throws an exception.

You might be tempted to just add writer.Close() at the end, but what happens if writer.WriteLine throws an exception? The code will jump straight to your catch block, skipping the Close() call entirely, and you're right back where you started with a locked file.

Letting Go with the Using Statement

The professional way to handle this in C# is the using statement. It ensures that the IDisposable object (like our writer) is cleaned up and the file is closed, even if the code crashes halfway through.

public void LogEvent(string message)
{
    using (StreamWriter writer = new StreamWriter("log.txt", true))
    {
        writer.WriteLine($"{DateTime.Now}: {message}");
    } 
    // The file is guaranteed to be closed here, no matter what.
}

In modern C# (8.0+), you can make this even cleaner with a using declaration. By removing the curly braces and just putting using before the variable, the object is disposed of the moment it goes out of scope (usually at the end of the method). I personally prefer this style because it reduces the "pyramid of doom" indentation in your code.

Quick Wins with File Static Methods

Now, if you aren't doing something complex—like streaming a massive file line-by-line—you probably shouldn't be using StreamWriter or StreamReader at all. The System.IO.File class provides static helper methods that handle the opening, writing, and closing in one single line of code.

  • File.WriteAllText(path, contents): Overwrites the file with a string.
  • File.AppendAllText(path, contents): Adds text to the end of the file (perfect for our logger).
  • File.ReadAllText(path): Slurps the entire file into a single string.
  • File.ReadAllLines(path): Reads the file into a string[] array.

Just a word of caution: ReadAllText and ReadAllLines are convenient, but they load the entire file into your RAM. If you're dealing with a 2GB log file, your app will likely crash. In those cases, go back to the StreamReader and process the file line-by-line using a while loop.

Avoiding Pathing Disasters

One last thing: stop hardcoding slashes in your paths. If you write "C:\\Logs\\log.txt", your code will break the moment you try to run it on Linux or macOS. C# gives us Path.Combine to handle this for us. It automatically detects which directory separator the current OS uses.

string folder = "Logs";
string fileName = "app_log.txt";
string fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, folder, fileName);

// Also, always make sure the directory actually exists before writing to it!
if (!Directory.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, folder)))
{
    Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, folder));
}



📋 Practical Task

Building a Simple Config-File Tracker

Your task is to create a small utility that manages a "Last Run" configuration file. This will test your ability to write, read, and handle paths correctly.

Requirements:

  • Create a folder named Settings in the application's base directory.
  • Create a file inside that folder called config.txt.
  • The program should:
    1. Read the current contents of config.txt (if it exists) and print them to the console.
    2. Write the current date and time to config.txt, overwriting the previous value.
    3. Print a confirmation message to the console stating that the timestamp has been updated.
  • Use Path.Combine for all path operations and ensure you use the using statement or File static methods to avoid locking the file.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.