Monitor Directory Changes in C# with FileSystemWatcher

The FileSystemWatcher class in .NET provides a built-in mechanism to listen to file system change notifications and react in real time when files or directories are created, modified, deleted, or renamed. This article explains what FileSystemWatcher is, how to configure and implement it in C#, and how to handle common edge cases like duplicate events and buffer overflows.

What Is FileSystemWatcher?

FileSystemWatcher is a component located in the System.IO namespace. It hooks into the underlying operating system's file notification system (such as the Win32 ReadDirectoryChangesW API on Windows) to track changes in a specified directory. Instead of manually polling a folder at regular intervals, which consumes unnecessary CPU and disk resources, FileSystemWatcher raises asynchronous events immediately when a file system change occurs.

Essential Properties and Events

To use FileSystemWatcher effectively, you must configure its target location, what types of changes to listen for, and which events to subscribe to.

Core Properties

  • Path: The absolute or relative path of the directory you want to monitor.
  • Filter: A search string used to match specific files (e.g., *.txt, data_*.json). To monitor all files, set it to *.* or leave it default.
  • IncludeSubdirectories: A boolean indicating whether child directories inside the target path should also be monitored.
  • NotifyFilter: A bitwise combination of NotifyFilters flags specifying what attributes trigger an event (e.g., FileName, DirectoryName, LastWrite, Size).
  • EnableRaisingEvents: A boolean that starts (true) or stops (false) the component from raising events.

Core Events

  • Created: Triggered when a new file or directory is created.
  • Changed: Triggered when a file or directory is modified (e.g., content changes, attribute changes).
  • Deleted: Triggered when a file or directory is deleted.
  • Renamed: Triggered when a file or directory changes its name.
  • Error: Triggered when the internal buffer overflows or a system error occurs.

Implementing FileSystemWatcher in C

Below is a complete implementation showing how to initialize, configure, and process file notifications in a console application.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string directoryToWatch = @"C:\MonitoredFolder";

        if (!Directory.Exists(directoryToWatch))
        {
            Directory.CreateDirectory(directoryToWatch);
        }

        using var watcher = new FileSystemWatcher(directoryToWatch);

        // Configure what changes to watch
        watcher.NotifyFilter = NotifyFilters.FileName
                             | NotifyFilters.DirectoryName
                             | NotifyFilters.LastWrite
                             | NotifyFilters.Size;

        // Monitor only text files (use "*.*" for all files)
        watcher.Filter = "*.txt";
        watcher.IncludeSubdirectories = false;

        // Attach event handlers
        watcher.Created += OnCreated;
        watcher.Changed += OnChanged;
        watcher.Deleted += OnDeleted;
        watcher.Renamed += OnRenamed;
        watcher.Error += OnError;

        // Start listening
        watcher.EnableRaisingEvents = true;

        Console.WriteLine($"Watching directory: {directoryToWatch}. Press 'q' to exit.");
        while (Console.ReadKey().KeyChar != 'q') ;
    }

    private static void OnCreated(object sender, FileSystemEventArgs e) =>
        Console.WriteLine($"[Created] {e.FullPath}");

    private static void OnChanged(object sender, FileSystemEventArgs e) =>
        Console.WriteLine($"[Changed] {e.FullPath}");

    private static void OnDeleted(object sender, FileSystemEventArgs e) =>
        Console.WriteLine($"[Deleted] {e.FullPath}");

    private static void OnRenamed(object sender, RenamedEventArgs e) =>
        Console.WriteLine($"[Renamed] {e.OldFullPath} -> {e.FullPath}");

    private static void OnError(object sender, ErrorEventArgs e) =>
        Console.WriteLine($"[Error] {e.GetException().Message}");
}

Common Challenges and Best Practices

1. Handling Duplicate Events

Operating systems often trigger multiple Changed notifications for a single file operation (for example, when file contents and metadata like LastWriteTime are written in separate steps).

To prevent redundant processing:

  • Implement a short debounce mechanism using a memory cache or a Dictionary that tracks the last processed timestamp for a file.
  • Ignore any duplicate events for the same file that arrive within a short threshold (e.g., 200–500 milliseconds).

2. File Locking

When the Created event fires, the application that created the file may still have an exclusive write lock on it. Attempting to open or read the file immediately can throw an IOException.

To resolve this:

  • Wrap file access inside a retry loop with exponential backoff.
  • Catch IOException and delay for a few hundred milliseconds before attempting to read again.

3. Preventing Buffer Overflows

FileSystemWatcher relies on an internal buffer to hold events before delivering them to your code. If thousands of files change simultaneously and your event handlers take too long to complete, the buffer overflows, and changes are lost.

To avoid buffer drops:

  • Keep event handlers extremely lightweight: offload file processing to a background worker or a channel (System.Threading.Channels.Channel<T>).
  • Increase the buffer size via watcher.InternalBufferSize (default is 8 KB, can be increased up to 64 KB) if burst file activity is expected.
  • Always subscribe to the Error event to detect when a buffer overflow occurs.