Skip to main content

C# File System Watcher

Introduction

When developing applications that interact with the file system, you might need to respond to file or directory changes as they occur. For instance, you may want to automatically process new files as they're added to a directory or update application data when a configuration file changes. Manually checking for these changes can be inefficient and resource-intensive.

C# provides an elegant solution to this problem through the FileSystemWatcher class, which allows your application to monitor file system changes in real-time. This component listens for file system events and raises notifications when files or directories in the specified path are created, modified, renamed, or deleted.

In this tutorial, we'll explore how to use the FileSystemWatcher class to monitor file system changes efficiently in your C# applications.

Prerequisites

  • Basic knowledge of C# programming
  • Understanding of file handling concepts
  • Familiarity with event-driven programming

The FileSystemWatcher Class

The FileSystemWatcher class is part of the System.IO namespace and provides a way to listen for file system change notifications. Before we dive into code examples, let's understand its key properties and methods:

Key Properties

  • Path: The directory to monitor
  • Filter: The type of files to watch (e.g., "*.txt" for text files only)
  • IncludeSubdirectories: Determines whether to monitor subdirectories
  • NotifyFilter: Specifies the types of changes to watch for (e.g., file name, size, attributes)
  • EnableRaisingEvents: Controls whether the component is actively monitoring

Key Events

  • Created: Triggered when a file or directory is created
  • Changed: Triggered when a file or directory is changed
  • Deleted: Triggered when a file or directory is deleted
  • Renamed: Triggered when a file or directory is renamed
  • Error: Triggered when the watcher experiences an error

Basic FileSystemWatcher Example

Let's start with a simple example that monitors a directory for all file changes:

csharp
using System;
using System.IO;
using System.Threading;

class Program
{
static void Main(string[] args)
{
// Create a new FileSystemWatcher
using (FileSystemWatcher watcher = new FileSystemWatcher())
{
// Set the directory to monitor
watcher.Path = @"C:\WatchFolder";

// Watch for changes in LastWrite and LastAccess time, and
// the creation, deletion, or renaming of files
watcher.NotifyFilter = NotifyFilters.LastWrite
| NotifyFilters.LastAccess
| NotifyFilters.FileName
| NotifyFilters.DirectoryName;

// Only watch text files
watcher.Filter = "*.txt";

// Add event handlers
watcher.Created += OnFileChanged;
watcher.Changed += OnFileChanged;
watcher.Deleted += OnFileChanged;
watcher.Renamed += OnFileRenamed;

// Begin watching
watcher.EnableRaisingEvents = true;

// Keep the program running
Console.WriteLine("Press 'q' to quit the sample.");
while (Console.Read() != 'q') ;
}
}

// Event handlers
private static void OnFileChanged(object source, FileSystemEventArgs e)
{
// Specify what is done when a file is changed, created, or deleted
Console.WriteLine($"File: {e.FullPath} {e.ChangeType}");
}

private static void OnFileRenamed(object source, RenamedEventArgs e)
{
// Specify what is done when a file is renamed
Console.WriteLine($"File: {e.OldFullPath} renamed to {e.FullPath}");
}
}

Example Output

When files are modified in the watched directory, you'll see output like this:

Press 'q' to quit the sample.
File: C:\WatchFolder\document.txt Created
File: C:\WatchFolder\document.txt Changed
File: C:\WatchFolder\document.txt renamed to C:\WatchFolder\important_document.txt
File: C:\WatchFolder\important_document.txt Deleted

Understanding NotifyFilter Options

The NotifyFilter property allows you to specify which changes you want to monitor. Here are the available options:

csharp
watcher.NotifyFilter = NotifyFilters.LastWrite     // Content changes
| NotifyFilters.LastAccess // Last access time changes
| NotifyFilters.FileName // File name changes
| NotifyFilters.DirectoryName // Directory name changes
| NotifyFilters.Attributes // Attribute changes
| NotifyFilters.Size // Size changes
| NotifyFilters.Security // Security changes
| NotifyFilters.CreationTime; // Creation time changes

Choose the appropriate filters based on the specific changes you need to monitor.

Monitoring Subdirectories

If you want to monitor files in subdirectories as well, you can set the IncludeSubdirectories property to true:

csharp
watcher.IncludeSubdirectories = true;

Handling FileSystemWatcher Events

Let's examine how to handle each event type:

Created Event

csharp
watcher.Created += (sender, e) =>
{
Console.WriteLine($"Created: {e.FullPath}");

// Example: Process a newly created file
if (File.Exists(e.FullPath))
{
try
{
string contents = File.ReadAllText(e.FullPath);
Console.WriteLine($"New file contains: {contents}");
}
catch (IOException)
{
// File might still be locked by the creating process
Console.WriteLine("Could not immediately read the file - it may be locked.");
}
}
};

Changed Event

csharp
watcher.Changed += (sender, e) =>
{
Console.WriteLine($"Changed: {e.FullPath}");

// The Changed event may fire multiple times for a single action
// Consider using a cooldown period if needed
};

Deleted Event

csharp
watcher.Deleted += (sender, e) =>
{
Console.WriteLine($"Deleted: {e.FullPath}");

// Example: Clean up resources associated with the deleted file
// Note: The file no longer exists at this point
};

Renamed Event

csharp
watcher.Renamed += (sender, e) =>
{
Console.WriteLine($"Renamed:");
Console.WriteLine($" Old: {e.OldFullPath}");
Console.WriteLine($" New: {e.FullPath}");

// Example: Update references to the renamed file
};

Error Event

csharp
watcher.Error += (sender, e) =>
{
// Handle errors that occur during monitoring
Exception ex = e.GetException();
Console.WriteLine($"Error: {ex.Message}");

// You might want to restart the watcher after an error
};

Real-World Application: Auto-Processing Files

Here's a practical example that automatically processes text files when they're added to a directory:

csharp
using System;
using System.IO;
using System.Threading;

class LogProcessor
{
private readonly FileSystemWatcher _watcher;
private readonly string _processingDirectory;
private readonly string _processedDirectory;

public LogProcessor(string watchDirectory)
{
// Verify directories exist
if (!Directory.Exists(watchDirectory))
{
throw new DirectoryNotFoundException($"Directory not found: {watchDirectory}");
}

_processingDirectory = Path.Combine(watchDirectory, "processing");
_processedDirectory = Path.Combine(watchDirectory, "processed");

// Create subdirectories if they don't exist
Directory.CreateDirectory(_processingDirectory);
Directory.CreateDirectory(_processedDirectory);

// Create and configure the watcher
_watcher = new FileSystemWatcher
{
Path = watchDirectory,
Filter = "*.log",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
EnableRaisingEvents = false
};

// Add event handler for created files
_watcher.Created += OnFileCreated;
}

public void Start()
{
// Process any existing files
foreach (string file in Directory.GetFiles(_watcher.Path, "*.log"))
{
ProcessFile(file);
}

// Start monitoring for new files
_watcher.EnableRaisingEvents = true;
Console.WriteLine("Log processor started. Watching for log files...");
}

public void Stop()
{
_watcher.EnableRaisingEvents = false;
_watcher.Dispose();
Console.WriteLine("Log processor stopped.");
}

private void OnFileCreated(object sender, FileSystemEventArgs e)
{
ProcessFile(e.FullPath);
}

private void ProcessFile(string filePath)
{
try
{
string fileName = Path.GetFileName(filePath);
string processingPath = Path.Combine(_processingDirectory, fileName);
string processedPath = Path.Combine(_processedDirectory, fileName);

Console.WriteLine($"Processing file: {fileName}");

// Move to processing directory
File.Move(filePath, processingPath);

// Simulate processing time
Thread.Sleep(1000);

// Read and process the file
string[] lines = File.ReadAllLines(processingPath);

// Create processed file with uppercase content
using (StreamWriter writer = new StreamWriter(processedPath))
{
writer.WriteLine($"Processed on: {DateTime.Now}");
writer.WriteLine("------------------------");

foreach (string line in lines)
{
// Example processing: convert to uppercase
writer.WriteLine(line.ToUpper());
}
}

// Delete original file after processing
File.Delete(processingPath);

Console.WriteLine($"Successfully processed: {fileName}");
}
catch (IOException ex)
{
Console.WriteLine($"Error processing file: {ex.Message}");
}
}
}

class Program
{
static void Main(string[] args)
{
string watchDirectory = @"C:\LogFiles";

try
{
LogProcessor processor = new LogProcessor(watchDirectory);
processor.Start();

Console.WriteLine("Press 'q' to quit the sample.");
while (Console.Read() != 'q') ;

processor.Stop();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}

This example demonstrates a log file processor that:

  1. Watches for new .log files
  2. Moves them to a processing directory
  3. Processes their content (in this case, converting to uppercase)
  4. Saves the processed files to a processed directory

Best Practices and Considerations

When using FileSystemWatcher, keep these best practices in mind:

1. Handle Multiple Events

The Changed event might fire multiple times for a single operation. Consider implementing a cooldown period:

csharp
private DateTime _lastEventTime = DateTime.MinValue;
private readonly object _lockObject = new object();

watcher.Changed += (sender, e) =>
{
lock (_lockObject)
{
// Ignore events that fire within 500ms of each other
if (DateTime.Now.Subtract(_lastEventTime).TotalMilliseconds < 500)
{
return;
}

_lastEventTime = DateTime.Now;
Console.WriteLine($"File changed: {e.FullPath}");

// Handle the event
}
};

2. Handle File Access Errors

Files might be locked for a short period when they're being written to:

csharp
watcher.Created += (sender, e) =>
{
bool fileReady = false;
int attempts = 0;

while (!fileReady && attempts < 5)
{
try
{
// Try to open the file for exclusive read
using (FileStream fs = File.Open(e.FullPath, FileMode.Open, FileAccess.Read, FileShare.None))
{
// If we get here, the file is no longer locked
fileReady = true;
}
}
catch (IOException)
{
// File is still locked, wait and try again
attempts++;
Thread.Sleep(100 * attempts);
}
}

if (fileReady)
{
// Process the file
string content = File.ReadAllText(e.FullPath);
Console.WriteLine($"Processed file: {e.FullPath}");
}
else
{
Console.WriteLine($"Could not access file after several attempts: {e.FullPath}");
}
};

3. Dispose Resources Properly

Always dispose of the FileSystemWatcher when you're done with it:

csharp
using (FileSystemWatcher watcher = new FileSystemWatcher())
{
// Configure and use the watcher

// It will be automatically disposed when leaving the block
}

4. Performance Considerations

  • Be selective with NotifyFilter to reduce unnecessary event handling
  • Use specific Filter patterns instead of monitoring all files
  • Avoid monitoring directories with high activity
  • Consider setting buffer size for high-volume scenarios:
csharp
watcher.InternalBufferSize = 65536; // 64KB buffer

Common Pitfalls

  1. Network Paths: FileSystemWatcher can be unreliable on network paths. Consider alternative approaches for remote directories.

  2. Missing Events: Under heavy load, some events might be missed. Design your application to handle this possibility.

  3. Multiple Notifications: As mentioned earlier, some operations can trigger multiple events.

  4. Large Directory Trees: Monitoring large directory structures with IncludeSubdirectories can consume significant resources.

Summary

The FileSystemWatcher class provides a powerful way to monitor file system changes in real-time, allowing your applications to respond automatically to file events. We've covered:

  • Setting up and configuring a FileSystemWatcher
  • Handling different file system events
  • Implementing practical file processing solutions
  • Best practices for reliable file system monitoring

By using FileSystemWatcher effectively, you can create responsive applications that efficiently process files as they change, without resorting to inefficient polling methods.

Additional Resources

Exercises

  1. Create a simple text file monitor that logs all changes to a specific directory.
  2. Build an application that automatically backs up files when they're modified.
  3. Create an image processing pipeline that automatically resizes images when they're added to a directory.
  4. Implement a configuration file watcher that reloads application settings whenever the configuration file changes.
  5. Extend the log processor example to handle different file formats and processing rules.

Happy coding!



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)