Introduction
FileSystemWatcher.Filter only supports a single pattern (e.g., "*.txt"). To watch multiple file types, use FileSystemWatcher.Filters (plural, .NET 6+) which accepts multiple patterns, or create multiple FileSystemWatcher instances (one per extension), or use a single watcher with Filter = "*.*" and filter events manually in the event handler. The Filters collection is the cleanest approach on modern .NET.
Method 1: Filters Collection (.NET 6+)
.NET 6 added a Filters collection that accepts multiple patterns:
1using System.IO;
2
3var watcher = new FileSystemWatcher(@"C:\Documents")
4{
5 EnableRaisingEvents = true,
6 NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
7};
8
9// Add multiple filters
10watcher.Filters.Add("*.txt");
11watcher.Filters.Add("*.csv");
12watcher.Filters.Add("*.xml");
13
14watcher.Created += (sender, e) =>
15 Console.WriteLine($"Created: {e.FullPath}");
16
17watcher.Changed += (sender, e) =>
18 Console.WriteLine($"Changed: {e.FullPath}");
19
20Console.ReadLine();
This is the recommended approach for .NET 6 and later.
Method 2: Multiple FileSystemWatcher Instances
Create one watcher per file type (works on all .NET versions):
1using System.IO;
2
3string[] extensions = { "*.txt", "*.csv", "*.xml", "*.json" };
4var watchers = new List<FileSystemWatcher>();
5
6foreach (var ext in extensions)
7{
8 var watcher = new FileSystemWatcher(@"C:\Documents")
9 {
10 Filter = ext,
11 EnableRaisingEvents = true,
12 NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
13 };
14
15 watcher.Created += OnFileCreated;
16 watcher.Changed += OnFileChanged;
17 watcher.Deleted += OnFileDeleted;
18 watcher.Renamed += OnFileRenamed;
19
20 watchers.Add(watcher);
21}
22
23void OnFileCreated(object sender, FileSystemEventArgs e)
24 => Console.WriteLine($"Created: {e.FullPath}");
25
26void OnFileChanged(object sender, FileSystemEventArgs e)
27 => Console.WriteLine($"Changed: {e.FullPath}");
28
29void OnFileDeleted(object sender, FileSystemEventArgs e)
30 => Console.WriteLine($"Deleted: {e.FullPath}");
31
32void OnFileRenamed(object sender, RenamedEventArgs e)
33 => Console.WriteLine($"Renamed: {e.OldFullPath} → {e.FullPath}");
34
35// Keep watchers alive
36Console.ReadLine();
37
38// Cleanup
39foreach (var w in watchers)
40 w.Dispose();
Method 3: Watch All Files and Filter in Handler
Use Filter = "*.*" or Filter = "" (empty string watches everything) and check the extension in the event handler:
1using System.IO;
2
3var allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
4{
5 ".txt", ".csv", ".xml", ".json"
6};
7
8var watcher = new FileSystemWatcher(@"C:\Documents")
9{
10 Filter = "*.*", // Watch all files
11 EnableRaisingEvents = true,
12 IncludeSubdirectories = true,
13 NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
14};
15
16watcher.Created += (sender, e) =>
17{
18 string ext = Path.GetExtension(e.FullPath);
19 if (allowedExtensions.Contains(ext))
20 {
21 Console.WriteLine($"Created: {e.FullPath}");
22 }
23};
24
25watcher.Changed += (sender, e) =>
26{
27 string ext = Path.GetExtension(e.FullPath);
28 if (allowedExtensions.Contains(ext))
29 {
30 Console.WriteLine($"Changed: {e.FullPath}");
31 }
32};
This is the simplest approach but raises more events (all file types) and filters them afterward.
Reusable MultiFilterWatcher Class
1using System.IO;
2
3public class MultiFilterWatcher : IDisposable
4{
5 private readonly FileSystemWatcher _watcher;
6 private readonly HashSet<string> _extensions;
7
8 public event FileSystemEventHandler Created;
9 public event FileSystemEventHandler Changed;
10 public event FileSystemEventHandler Deleted;
11 public event RenamedEventHandler Renamed;
12
13 public MultiFilterWatcher(string path, params string[] extensions)
14 {
15 _extensions = new HashSet<string>(
16 extensions.Select(e => e.StartsWith(".") ? e : "." + e),
17 StringComparer.OrdinalIgnoreCase
18 );
19
20 _watcher = new FileSystemWatcher(path)
21 {
22 Filter = "*.*",
23 EnableRaisingEvents = true,
24 NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
25| NotifyFilters.CreationTime }; _watcher.Created += (s, e) => { if (IsMatch(e.FullPath)) Created?.Invoke(s, e); }; _watcher.Changed += (s, e) => { if (IsMatch(e.FullPath)) Changed?.Invoke(s, e); }; _watcher.Deleted += (s, e) => { if (IsMatch(e.FullPath)) Deleted?.Invoke(s, e); }; _watcher.Renamed += (s, e) => { if (IsMatch(e.FullPath)) Renamed?.Invoke(s, e); }; } private bool IsMatch(string path) => _extensions.Contains(Path.GetExtension(path)); public void Dispose() => _watcher.Dispose(); } // Usage using var watcher = new MultiFilterWatcher(@"C:\Documents", ".txt", ".csv", ".json"); watcher.Created += (s, e) => Console.WriteLine($"New file: {e.Name}"); watcher.Changed += (s, e) => Console.WriteLine($"Modified: {e.Name}"); ``` ## Handling Duplicate Events `FileSystemWatcher` often fires multiple events for a single file operation. Debounce with a timer: ```csharp using System.Collections.Concurrent; var recentEvents = new ConcurrentDictionary<string, DateTime>(); watcher.Changed += (sender, e) => { string ext = Path.GetExtension(e.FullPath); if (!allowedExtensions.Contains(ext)) return; var now = DateTime.UtcNow; if (recentEvents.TryGetValue(e.FullPath, out var lastTime) && (now - lastTime).TotalMilliseconds < 500) { return; // Duplicate event within 500ms — skip } recentEvents[e.FullPath] = now; Console.WriteLine($"Changed: {e.FullPath}"); }; ``` ## Common Pitfalls * **Assuming `Filter` accepts multiple patterns separated by `|` or `;`**: Unlike some other APIs, `FileSystemWatcher.Filter` only accepts a single pattern. Passing `"*.txt|*.csv"` does not work — it matches nothing. Use `Filters` (plural, .NET 6+) or multiple watchers. * **Not handling duplicate `Changed` events**: Saving a file in many editors triggers multiple `Changed` events (write, flush, close). Without debouncing, your handler processes the same file change 2-3 times. Use a timestamp-based deduplication strategy. * **Setting `EnableRaisingEvents = false` by default**: `FileSystemWatcher` does not raise events until `EnableRaisingEvents` is set to `true`. Forgetting this property is a common reason why watchers appear to do nothing. * **Not disposing watchers when no longer needed**: `FileSystemWatcher` uses system resources (file handles, kernel objects). Failing to dispose watchers in long-running applications can exhaust OS file notification limits. * **Watching network drives or very large directories**: `FileSystemWatcher` may miss events on network shares due to network latency or OS buffering limits. Increase `InternalBufferSize` (default 8KB, max 64KB) for high-volume directories, and use polling as a fallback for network paths. ## Summary * .NET 6+: Use `watcher.Filters.Add("*.txt")` for multiple patterns (cleanest approach) * All .NET versions: Create multiple `FileSystemWatcher` instances, one per extension * Alternative: Watch `"*.*"` and filter by extension in the event handler * Debounce duplicate events with a timestamp dictionary * Set `EnableRaisingEvents = true` to start receiving notifications