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.
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:
- Write to a temp file
- Delete the original
- Rename the temp file
- 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
Errorevent 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
ChangedandCreated. - Network drives: FileSystemWatcher is unreliable on network paths. Use polling (
Directory.GetFileson a timer) as a fallback. - Thread safety: Events fire on a thread pool thread, not the UI thread. Use
Dispatcher.Invokein 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();