All Resources
Code Snippet.NET2026-03-15

File System Watcher Pattern in C#

Monitor directories for file changes with debouncing and error recovery — the production-hardened pattern that handles FileSystemWatcher's notorious quirks.

csharp
using System.IO;
using System.Timers;

public class FileChangeMonitor : IDisposable
{
    private readonly FileSystemWatcher _watcher;
    private readonly Timer _debounce;
    private string? _lastPath;

    public event Action<string, WatcherChangeTypes>? FileChanged;

    public FileChangeMonitor(string path, string filter = "*.*")
    {
        _watcher = new FileSystemWatcher(path, filter)
        {
            NotifyFilter = NotifyFilters.FileName
                         | NotifyFilters.LastWrite
                         | NotifyFilters.Size,
            IncludeSubdirectories = true,
            InternalBufferSize = 64 * 1024 // 64KB buffer
        };

        _watcher.Changed += OnFileEvent;
        _watcher.Created += OnFileEvent;
        _watcher.Deleted += OnFileEvent;
        _watcher.Renamed += (s, e) =>
            FileChanged?.Invoke(e.FullPath, WatcherChangeTypes.Renamed);
        _watcher.Error += OnError;

        // Debounce timer — coalesce rapid duplicate events
        _debounce = new Timer(500) { AutoReset = false };
        _debounce.Elapsed += (s, e) =>
        {
            if (_lastPath != null)
                FileChanged?.Invoke(_lastPath, WatcherChangeTypes.Changed);
        };
    }

    private void OnFileEvent(object sender, FileSystemEventArgs e)
    {
        _lastPath = e.FullPath;
        _debounce.Stop();
        _debounce.Start(); // Restart the debounce window
    }

    private void OnError(object sender, ErrorEventArgs e)
    {
        // Buffer overflow — restart the watcher
        _watcher.EnableRaisingEvents = false;
        Thread.Sleep(100);
        _watcher.EnableRaisingEvents = true;
    }

    public void Start() => _watcher.EnableRaisingEvents = true;
    public void Stop() => _watcher.EnableRaisingEvents = false;

    public void Dispose()
    {
        _debounce.Dispose();
        _watcher.Dispose();
    }
}

How It Works

FileSystemWatcher monitors a directory by subscribing to OS-level file change notifications. The key configuration is NotifyFilter — flags that control which changes trigger events:

  • FileName — file creation, deletion, and renaming
  • LastWrite — file content modifications
  • Size — file size changes (catches some edge cases LastWrite misses)

The InternalBufferSize controls how many events the OS can queue before overflow. Default is 8KB — we increase to 64KB for busy directories.

The Debounce Problem

FileSystemWatcher's biggest gotcha: a single file save often fires 2-4 duplicate events. This happens because most editors:

  1. Write to a temp file
  2. Delete the original
  3. Rename the temp file
  4. Update timestamps

Each step triggers a separate event. The debounce timer coalesces these into a single notification — it waits 500ms after the last event before firing FileChanged.

Common Pitfalls

  • Buffer overflow: If too many events queue up (bulk file operations), the Error event fires. Our handler restarts the watcher automatically.
  • Rename vs. delete+create: Some editors (like Visual Studio) use delete+create instead of overwrite. Subscribe to both Changed and Created.
  • Network drives: FileSystemWatcher is unreliable on network paths. Use polling (Directory.GetFiles on a timer) as a fallback.
  • Thread safety: Events fire on a thread pool thread, not the UI thread. Use Dispatcher.Invoke in WPF apps.

Real-World Use Case

Watching a SolidWorks project folder for part file changes:

var monitor = new FileChangeMonitor(@"C:\Projects\Assembly", "*.SLD*");
monitor.FileChanged += (path, change) =>
{
    Console.WriteLine($"{change}: {Path.GetFileName(path)}");
    // Trigger dependency re-scan, BOM update, etc.
};
monitor.Start();
C#.NETFileSystemWatcherEventsFile I/O