Solving the File Monitoring Challenge
If you've ever needed to process files as soon as they appear in a folder, you know the frustration of polling. Your application checks the directory every few seconds, wasting CPU cycles and introducing delays between when files arrive and when you process them. Users upload a document, but your system takes 10 seconds to notice it exists.
FileSystemWatcher eliminates polling by subscribing to operating system notifications. When files change, the OS tells your application immediately. You can react to new files in milliseconds instead of seconds, and your CPU stays idle until actual changes occur. This approach scales better and provides instant feedback to users.
You'll learn how to set up watchers for different change types, handle events reliably, filter notifications, and avoid common pitfalls that cause duplicate events or missed changes. By the end, you'll build robust file monitoring that works in production environments.
Creating Your First FileSystemWatcher
FileSystemWatcher monitors a directory for changes and raises events when files or subdirectories are created, modified, deleted, or renamed. You specify the path to watch and which types of changes to track. The watcher runs on a background thread and notifies you through event handlers.
Start by creating a watcher for a specific directory. You'll enable it to start monitoring, then handle events as they occur. The watcher continues monitoring until you disable it or dispose of the object.
using System.IO;
var watchFolder = @"C:\Temp\WatchFolder";
Directory.CreateDirectory(watchFolder);
using var watcher = new FileSystemWatcher(watchFolder);
// Configure what to watch for
watcher.NotifyFilter = NotifyFilters.FileName
| NotifyFilters.LastWrite
| NotifyFilters.Size;
// Watch for changes to all files
watcher.Filter = "*.*";
// Handle different event types
watcher.Created += (sender, e) =>
{
Console.WriteLine($"Created: {e.Name} at {DateTime.Now:HH:mm:ss.fff}");
};
watcher.Changed += (sender, e) =>
{
Console.WriteLine($"Changed: {e.Name} at {DateTime.Now:HH:mm:ss.fff}");
};
watcher.Deleted += (sender, e) =>
{
Console.WriteLine($"Deleted: {e.Name} at {DateTime.Now:HH:mm:ss.fff}");
};
// Start monitoring
watcher.EnableRaisingEvents = true;
Console.WriteLine($"Monitoring {watchFolder}");
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
Output (when you create/modify test.txt):
Monitoring C:\Temp\WatchFolder
Created: test.txt at 14:23:45.123
Changed: test.txt at 14:23:45.145
Changed: test.txt at 14:23:45.167
Notice the multiple Changed events for a single file save. Most applications write files in several steps, each triggering an event. You'll need to handle this duplication in real applications, which we'll cover shortly. The NotifyFilter controls which attribute changes trigger events, reducing noise from irrelevant changes.
Handling File Renames Correctly
Rename operations are special because they involve two file names: the old name and the new name. FileSystemWatcher provides a dedicated Renamed event with both values, making it easy to track files that move or change names. Many file operations you'd expect to be simple writes are actually renames under the hood.
When applications save files, they often write to a temporary file then rename it to the target name. This atomic operation prevents corruption if the write fails partway through. Your watcher sees this as a rename event, not a creation.
using System.IO;
var watchFolder = @"C:\Temp\WatchFolder";
Directory.CreateDirectory(watchFolder);
using var watcher = new FileSystemWatcher(watchFolder);
watcher.Filter = "*.*";
watcher.Renamed += (sender, e) =>
{
Console.WriteLine($"Renamed:");
Console.WriteLine($" Old: {e.OldName}");
Console.WriteLine($" New: {e.Name}");
Console.WriteLine($" Change Type: {e.ChangeType}");
};
watcher.Created += (sender, e) =>
{
Console.WriteLine($"Created: {e.Name}");
};
watcher.EnableRaisingEvents = true;
Console.WriteLine("Monitoring for renames...");
Console.WriteLine("Try renaming a file in the watched folder");
Console.ReadLine();
Output (when renaming document.txt to report.txt):
Monitoring for renames...
Renamed:
Old: document.txt
New: report.txt
Change Type: Renamed
The RenamedEventArgs provides both OldName and Name properties, letting you update database records or tracking structures. If you only handle Created and Deleted events, renames appear as delete-then-create sequences, which can confuse your logic and trigger unnecessary reprocessing.
Filtering Notifications Efficiently
Watching an entire directory with all file types generates many events you might not care about. Use Filter to watch specific file types and NotifyFilter to ignore irrelevant attribute changes. This reduces event volume and prevents your handlers from processing noise.
You can watch multiple file types by changing the filter at runtime or creating multiple watchers. Filtering at the source is more efficient than receiving all events and discarding most of them in your handlers.
using System.IO;
var watchFolder = @"C:\Temp\Documents";
Directory.CreateDirectory(watchFolder);
using var watcher = new FileSystemWatcher(watchFolder);
// Only watch for .txt and .log files
watcher.Filter = "*.txt";
// You can add multiple extensions by creating multiple watchers
using var logWatcher = new FileSystemWatcher(watchFolder);
logWatcher.Filter = "*.log";
// Only notify on these specific changes
watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
logWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
// Include subdirectories
watcher.IncludeSubdirectories = true;
logWatcher.IncludeSubdirectories = true;
var handler = (object sender, FileSystemEventArgs e) =>
{
var watcherObj = (FileSystemWatcher)sender;
Console.WriteLine($"[{watcherObj.Filter}] {e.ChangeType}: {e.Name}");
};
watcher.Created += handler;
watcher.Changed += handler;
logWatcher.Created += handler;
logWatcher.Changed += handler;
watcher.EnableRaisingEvents = true;
logWatcher.EnableRaisingEvents = true;
Console.WriteLine("Monitoring .txt and .log files (including subdirectories)");
Console.ReadLine();
Output (when creating files):
Monitoring .txt and .log files (including subdirectories)
[*.txt] Created: notes.txt
[*.log] Created: app.log
[*.txt] Created: subfolder\readme.txt
Setting IncludeSubdirectories to true monitors the entire directory tree. The e.Name property includes the relative path from the watched folder, so you can see exactly which subdirectory contains the changed file. This is perfect for monitoring upload folders where users can create their own subdirectories.
Common Pitfalls and Solutions
Duplicate events are the most common issue. When you save a file, many applications fire multiple Changed events because they update the file in stages. You might see three or four events for a single user action. To handle this, implement a debouncing mechanism that waits for events to stop before processing. Use a timer that resets with each event, and only process when the timer expires without new events.
Another trap is accessing files immediately in event handlers. When the Created event fires, the file might still be locked by the application writing it. If you try to read it immediately, you'll get IOException. Always wrap file access in retry logic with short delays, or use FileStream options that allow shared reading. Some developers queue file paths and process them on a background thread with retry logic.
Buffer overflows occur when events arrive faster than you can process them. The FileSystemWatcher has an internal buffer that fills up quickly in busy directories. When it overflows, the Error event fires and you miss notifications. Increase InternalBufferSize from the default 8KB to 64KB for busy folders. More importantly, process events quickly by queueing work instead of doing heavy processing in event handlers.
Network paths and remote drives cause unreliable notifications. FileSystemWatcher depends on OS notifications, which don't work consistently over network shares due to caching and latency. If you must monitor network locations, consider periodic polling instead of FileSystemWatcher. For local files synced to cloud storage, the watcher works because changes happen locally first.
Try It Yourself
Build a file processor that automatically processes new text files and moves them to a completed folder. This pattern appears in many real-world scenarios like document processing, file imports, and batch operations.
using System.IO;
var inputFolder = @"C:\Temp\Input";
var processedFolder = @"C:\Temp\Processed";
Directory.CreateDirectory(inputFolder);
Directory.CreateDirectory(processedFolder);
var fileProcessor = new FileProcessor(inputFolder, processedFolder);
Console.WriteLine($"Monitoring: {inputFolder}");
Console.WriteLine($"Processed files go to: {processedFolder}");
Console.WriteLine("Drop .txt files into the input folder");
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
fileProcessor.Dispose();
class FileProcessor : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly string _processedFolder;
private readonly Dictionary<string, Timer> _timers = new();
private readonly object _lock = new();
public FileProcessor(string inputFolder, string processedFolder)
{
_processedFolder = processedFolder;
_watcher = new FileSystemWatcher(inputFolder)
{
Filter = "*.txt",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
};
_watcher.Created += OnFileChanged;
_watcher.Changed += OnFileChanged;
_watcher.EnableRaisingEvents = true;
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
lock (_lock)
{
// Debounce: reset timer for this file
if (_timers.TryGetValue(e.FullPath, out var existingTimer))
{
existingTimer.Change(500, Timeout.Infinite);
}
else
{
var timer = new Timer(_ => ProcessFile(e.FullPath),
null, 500, Timeout.Infinite);
_timers[e.FullPath] = timer;
}
}
}
private void ProcessFile(string filePath)
{
try
{
// Wait a moment for file to be fully written
Thread.Sleep(100);
// Read with shared access in case writer still has handle
using var stream = new FileStream(filePath, FileMode.Open,
FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
var content = reader.ReadToEnd();
var lineCount = content.Split('\n').Length;
Console.WriteLine($"\nProcessed: {Path.GetFileName(filePath)}");
Console.WriteLine($" Lines: {lineCount}");
// Move to processed folder
var destPath = Path.Combine(_processedFolder,
Path.GetFileName(filePath));
// Close before moving
stream.Close();
File.Move(filePath, destPath, overwrite: true);
Console.WriteLine($" Moved to: {destPath}");
// Cleanup timer
lock (_lock)
{
if (_timers.Remove(filePath, out var timer))
{
timer.Dispose();
}
}
}
catch (IOException ex)
{
Console.WriteLine($"Error processing {filePath}: {ex.Message}");
// Could retry here
}
}
public void Dispose()
{
_watcher?.Dispose();
lock (_lock)
{
foreach (var timer in _timers.Values)
{
timer.Dispose();
}
_timers.Clear();
}
}
}
Output (when dropping test.txt into input folder):
Monitoring: C:\Temp\Input
Processed files go to: C:\Temp\Processed
Drop .txt files into the input folder
Processed: test.txt
Lines: 5
Moved to: C:\Temp\Processed\test.txt
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
This example demonstrates debouncing with timers to handle duplicate events, safe file reading with shared access, and proper cleanup. The processor waits 500ms after the last event before processing, which handles multiple change notifications gracefully.