Lyo.FileSystemWatcher 2.0.0

dotnet add package Lyo.FileSystemWatcher --version 2.0.0
                    
NuGet\Install-Package Lyo.FileSystemWatcher -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Lyo.FileSystemWatcher" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lyo.FileSystemWatcher" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Lyo.FileSystemWatcher" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Lyo.FileSystemWatcher --version 2.0.0
                    
#r "nuget: Lyo.FileSystemWatcher, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Lyo.FileSystemWatcher@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Lyo.FileSystemWatcher&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Lyo.FileSystemWatcher&version=2.0.0
                    
Install as a Cake Tool

Lyo.FileSystemWatcher

Snapshot-based .NET file watcher. Finds creates, deletes, changes, moves, and renames with debounce and SHA256 hashing.

Features

  • Snapshot-based change detection. Compares directory snapshots rather than relying only on FileSystemWatcher events.
  • Debouncing. Batches rapid changes so an event storm does not fire.
  • Hash-based move detection. Finds file moves and renames even when the file system does not report them.
  • Batch scan. ScanCompleted fires once per debounce with the previous tree, current tree, and every detected change. CurrentSnapshot exposes the last completed tree.
  • Thread-safe. Safe to call from multiple threads.
  • Metrics. Optional IMetrics hook (Lyo.Metrics).
  • Options. FileSystemWatcherOptions covers debounce, hashing, path comparison, subdirectory watching, and include/exclude regexes.
  • Errors. Snapshot and detection failures go to Error, and to ILogger when you pass one.
  • Cancellation. Long-running snapshot work accepts CancellationToken.
  • Logging. Microsoft.Extensions.Logging when an ILogger is passed.

Examples

Hook events

using Lyo.FileSystemWatcher;
using Lyo.FileSystemWatcher.Enums;

// Create a watcher for a directory
using var watcher = new FileSystemWatcher("C:\\MyDirectory");

// Subscribe to events
watcher.FileCreated += (sender, e) =>
{
    Console.WriteLine($"File created: {e.NewPath}");
};

watcher.FileDeleted += (sender, e) =>
{
    Console.WriteLine($"File deleted: {e.OldPath}");
};

watcher.FileMoved += (sender, e) =>
{
    Console.WriteLine($"File moved: {e.OldPath} -> {e.NewPath}");
};

watcher.DirectoryChanged += (sender, e) =>
{
    Console.WriteLine($"Directory changed: {e.NewPath}");
    Console.WriteLine($" Files: {e.OldFileCount} -> {e.NewFileCount}");
    Console.WriteLine($" Directories: {e.OldDirectoryCount} -> {e.NewDirCount}");
};

// Watch for any change
watcher.OnAnyChange += (sender, e) =>
{
    Console.WriteLine($"Change detected: {e.ChangeType} - {e.NewPath ?? e.OldPath}");
};

// Keep the application running
Console.ReadLine();

Set options

using Lyo.FileSystemWatcher;
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<FileSystemWatcher>();

var options = new FileSystemWatcherOptions
{
    IncludeSubdirectories = true, // Watch subdirectories
    DebounceTimerDelay = 500, // 500ms debounce delay
    EnableFileHashing = true, // Enable hash-based move detection
    PathComparison = StringComparison.OrdinalIgnoreCase, // Case-insensitive (Windows)
    EnableMetrics = true // Enable metrics collection
};

// Get metrics service (if using Lyo.Metrics)
var metrics = serviceProvider.GetService<IMetrics>();

using var watcher = new FileSystemWatcher("C:\\MyDirectory", options, logger, metrics);

// Handle errors
watcher.Error += (sender, ex) =>
{
    Console.WriteLine($"Watcher error: {ex.Message}");
};

// Subscribe to events...

Turn off hashing for speed

var options = new FileSystemWatcherOptions
{
    EnableFileHashing = false // Significantly faster on large directories
};

Change the debounce delay

var options = new FileSystemWatcherOptions
{
    DebounceTimerDelay = 100 // Lower = faster response, higher CPU
    // DebounceTimerDelay = 1000 // Higher = slower response, lower CPU
};

Case-sensitive paths (Linux/macOS)

var options = new FileSystemWatcherOptions
{
    PathComparison = StringComparison.Ordinal // Case-sensitive
};

React to file changes

using var watcher = new FileSystemWatcher("C:\\MyDirectory");

watcher.FileChanged += (sender, e) =>
{
    Console.WriteLine($"File changed: {e.NewPath}");
    // Process file change...
};

Console.ReadLine(); // Keep running

React to directory changes

using var watcher = new FileSystemWatcher("C:\\MyDirectory");

watcher.DirectoryChanged += (sender, e) =>
{
    var fileDelta = (e.NewFileCount ?? 0) - (e.OldFileCount ?? 0);
    var dirDelta = (e.NewDirCount ?? 0) - (e.OldDirectoryCount ?? 0);
    
    Console.WriteLine($"Directory {e.NewPath} changed:");
    Console.WriteLine($" Files: {e.OldFileCount} -> {e.NewFileCount} (delta: {fileDelta:+0;-0;0})");
    Console.WriteLine($" Directories: {e.OldDirectoryCount} -> {e.NewDirCount} (delta: {dirDelta:+0;-0;0})");
};

Include subdirectories

var options = new FileSystemWatcherOptions
{
    IncludeSubdirectories = true
};

using var watcher = new FileSystemWatcher("C:\\MyDirectory", options);

watcher.OnAnyChange += (sender, e) =>
{
    Console.WriteLine($"Change in {e.NewPath ?? e.OldPath}: {e.ChangeType}");
};

Options for high throughput

var options = new FileSystemWatcherOptions
{
    EnableFileHashing = false, // Disable hashing for speed
    DebounceTimerDelay = 1000, // Longer debounce for lower CPU
    IncludeSubdirectories = true
};

using var watcher = new FileSystemWatcher("C:\\LargeDirectory", options);

Add the watcher in DI

// In Startup.cs or Program.cs
services.AddSingleton<ILogger<FileSystemWatcher>>(sp =>
    sp.GetRequiredService<ILoggerFactory>().CreateLogger<FileSystemWatcher>());

services.AddSingleton<FileSystemWatcher>(sp =>
{
    var logger = sp.GetRequiredService<ILogger<FileSystemWatcher>>();
    var metrics = sp.GetService<IMetrics>();
    var options = new FileSystemWatcherOptions
    {
        EnableMetrics = true,
        IncludeSubdirectories = true
    };
    return new FileSystemWatcher("C:\\MyDirectory", options, logger, metrics);
});

Kinds of change

public enum ChangeTypeEnum
{
    Unknown = 0,
    Created = 1, // File or directory created
    Changed = 2, // File content modified or directory content changed
    Deleted = 3, // File or directory deleted
    Renamed = 4, // Renamed within same parent directory
    Moved = 5 // Moved to different parent directory
}

Wire metrics

using Lyo.Metrics;

// Register metrics service
services.AddLyoMetrics();

// Create watcher with metrics
var metrics = serviceProvider.GetRequiredService<IMetrics>();
var options = new FileSystemWatcherOptions { EnableMetrics = true };
var watcher = new FileSystemWatcher("C:\\MyDirectory", options, logger, metrics);

FileSystemWatcherOptions

Property Type Default Description
IncludeSubdirectories bool false Whether subdirectories are watched recursively
DebounceTimerDelay int 250 Debounce delay in milliseconds. Changes inside this delay are batched together
EnableFileHashing bool true Enable file hashing for move/rename detection. Turn off for better performance
PathComparison StringComparison OrdinalIgnoreCase String comparison for path operations. Use Ordinal on case-sensitive file systems
EnableMetrics bool false Enable metrics collection (needs an IMetrics instance)

File events

  • FileCreated. Raised when a file is created.
  • FileDeleted. Raised when a file is deleted.
  • FileChanged. Raised when a file's content is modified.
  • FileMoved. Raised when a file is moved to a different directory.
  • FileRenamed. Raised when a file is renamed (moved within the same directory).

Directory change events

  • DirectoryCreated. Raised when a directory is created.
  • DirectoryDeleted. Raised when a directory is deleted.
  • DirectoryChanged. Raised when a directory's content changes.
  • DirectoryMoved. Raised when a directory is moved to a different parent.
  • DirectoryRenamed. Raised when a directory is renamed (moved within the same parent).

General events

  • OnAnyChange. Raised for any file or directory change.
  • Error. Raised when snapshot or change detection fails.

Event data

Every event carries a FileSystemChangeInfo object with these properties:

public sealed record FileSystemChangeInfo(
    string? OldPath, // Previous path (null for created items)
    string? NewPath, // New path (null for deleted items)
    ChangeTypeEnum ChangeType, // Type of change
    bool IsDirectory, // True if directory, false if file
    int? OldFileCount = null, // Directory: files before change
    int? OldDirectoryCount = null,// Directory: subdirectories before change
    int? NewFileCount = null, // Directory: files after change
    int? NewDirCount = null) // Directory: subdirectories after change

Metrics integration

When EnableMetrics is true and an IMetrics instance is provided, these metrics are recorded:

Snapshot metrics

  • filesystemwatcher.snapshot.duration. Duration of snapshot operations (timing).
  • filesystemwatcher.snapshot.duration_ms. Snapshot duration in milliseconds (gauge).
  • filesystemwatcher.snapshot.file_count. Number of files in snapshot (gauge).
  • filesystemwatcher.snapshot.directory_count. Directory count in the snapshot (gauge).
  • filesystemwatcher.snapshot.item_count. Total items in snapshot (gauge).

Change detection metrics

  • filesystemwatcher.change_detection.duration. Change-detection duration (timing).
  • filesystemwatcher.change_detection.duration_ms. Change-detection duration in milliseconds (gauge).
  • filesystemwatcher.changes.detected. Changes detected per scan (gauge).

Event metrics

  • filesystemwatcher.file.created. File created events (counter).
  • filesystemwatcher.file.deleted. File deleted events (counter).
  • filesystemwatcher.file.changed. File changed events (counter).
  • filesystemwatcher.file.moved. File moved events (counter).
  • filesystemwatcher.file.renamed. File renamed events (counter).
  • filesystemwatcher.directory.created. Directory created events (counter).
  • filesystemwatcher.directory.deleted. Directory deleted events (counter).
  • filesystemwatcher.directory.changed. Directory changed events (counter).
  • filesystemwatcher.directory.moved. Directory moved events (counter).
  • filesystemwatcher.directory.renamed. Directory renamed events (counter).

Error metrics

  • filesystemwatcher.error.count. Number of errors encountered (counter).

Error handling

Subscribe to Error for snapshot and detection failures. Pass an ILogger and those errors are logged as well.

// Subscribe to error events
watcher.Error += (sender, ex) =>
{
    Console.WriteLine($"Error: {ex.Message}");
    // Handle error appropriately
};

// Errors are also logged if a logger is provided
var logger = loggerFactory.CreateLogger<FileSystemWatcher>();
var watcher = new FileSystemWatcher("C:\\MyDirectory", options, logger);

Typical failure modes:

  • Snapshot failures. Directory access denied, or disk errors.
  • Change detection. Memory pressure or cancellation.
  • Event handler exceptions. Caught and logged. They do not take down the watcher.

File hashing

  • Enabled (default). Accurate move/rename detection. Slower against large directories.
  • Disabled. Faster. Move detection relies on file system events only.

Memory usage

  • Snapshots keep the complete directory tree in memory
  • For very large directory structures (10,000+ files), consider these:
  • Disabling file hashing
  • Increasing debounce delay
  • Monitoring memory usage

Debounce delay

  • Lower values (50-100ms). Faster response, higher CPU usage.
  • Higher values (500-1000ms). Slower response, lower CPU usage.
  • Default (250ms). A reasonable default for most directories.

Expected snapshot times

  • Small directories (< 100 files). Under 100ms per snapshot.
  • Medium directories (100-1000 files). 100-500ms per snapshot.
  • Large directories (> 1000 files). 500ms+ per snapshot. Hashing large files stretches that further.

Thread safety

FileSystemWatcher is thread-safe and can be used from more than one thread:

// Safe to use from multiple threads
var watcher = new FileSystemWatcher("C:\\MyDirectory");

Task.Run(() => watcher.FileCreated += OnFileCreated);
Task.Run(() => watcher.FileDeleted += OnFileDeleted);

Disposal

Always dispose the watcher when you are done:

using var watcher = new FileSystemWatcher("C:\\MyDirectory");
// Use watcher...
// Automatically disposed when leaving scope

Or dispose by hand:

var watcher = new FileSystemWatcher("C:\\MyDirectory");
try
{
    // Use watcher...
}
finally
{
    watcher.Dispose();
}

Events are not firing

  • Check path exists. The directory must exist when the watcher is created.
  • Check permissions. Read access to the directory is required.
  • Check debounce delay. Very rapid changes may be batched.
  • Check event handlers. Subscribe before changes occur.
  • Wait for initial snapshot. The watcher needs time to take that first snapshot.

CPU is high

  • Disable file hashing. Set EnableFileHashing = false.
  • Increase debounce delay. Higher values reduce CPU usage.
  • Watch snapshot frequency. Too many rapid changes can spike CPU use.

Memory is high

  • Watch snapshot size. Large directory trees consume more memory.
  • Disable hashing. Reduces memory per file entry.
  • Dispose the watcher. Leaks show up if it is left running.

Move/rename events are missing

  • Enable file hashing. Needed for reliable move/rename detection.
  • Check the file system. Some file systems never report move events.
  • Check timing. Very rapid moves may show up as delete+create.
  • Directory moves. The directory name must stay the same (new parent, same name).

Known limitation: file move bug

There is a known bug: directory change events for the source directory when moving a file show incorrect counts. The destination directory works correctly.

Known limitations: performance

  • File hashing can be slow on large files or on many files
  • Snapshot operations are synchronous and can block for a moment
  • Very large directory structures consume significant memory

Known limitations: directory move detection

  • Directory move detection only works when the directory name stays the same and the parent changes
  • If both name and parent change, it is detected as delete + create

Architecture: snapshot-based detection

The watcher takes periodic snapshots of the directory tree and diffs them. That is more reliable than FileSystemWatcher events by themselves.

Architecture: debouncing

A debounce timer batches multiple rapid changes, which prevents event storms and reduces CPU usage.

Architecture: hash-based move detection

File hashing (SHA256) detects moves and renames even when the file system does not provide that information directly.

Architecture: error resilience

  • Event handler exceptions are caught and logged, so one faulty handler cannot crash the watcher
  • Snapshot errors are caught and reported through the Error event
  • Cancellation tokens allow graceful shutdown of long-running operations

Types

Type Description
FileSystemWatcher Snapshot-based, debounced watcher (IDisposable). Constructor: FileSystemWatcher(string path, FileSystemWatcherOptions?, ILogger?, IMetrics?). Raises the file/directory/OnAnyChange/ScanCompleted/Error events listed above.
FileSystemWatcherOptions IncludeSubdirectories, DebounceTimerDelay, EnableFileHashing, PathComparison, EnableMetrics, IncludePatterns, ExcludePatterns.
FileSystemChangeInfo record payload emitted by every change event.
ChangeTypeEnum Unknown / Created / Changed / Deleted / Renamed / Moved.
DirectorySnapshotEntry Single snapshot entry (path, info, optional Hash, Fingerprint, FileSize).
SnapshotTree / SnapshotDirectoryNode In-memory snapshot of the watched tree, used for diffing.
FileSystemScanCompletedEventArgs / ScanCompleted One event per debounce with previous tree, current tree, and the full change list. CurrentSnapshot is the last completed tree.
FileSystemWatcherMapping ToDto() extensions that copy size/timestamps onto persistable models (relative '/' paths).
Constants.Metrics + Constants.Metrics.Tags Metric and tag name constants (snapshot, change detection, event, and error metrics above).
Utilities Helpers the watcher implementation shares.

Dependencies

Generated from ProjectReference / PackageReference (same model as docs/Lyo.ProjectGraph.html).

  • Lyo.Common.Core (direct, lyo)
  • Lyo.FileSystemWatcher.Models (direct, lyo)
  • Lyo.Hashing (direct, lyo)
  • Lyo.Metrics (direct, lyo)
  • Microsoft.Extensions.Logging.Abstractions 10.0.5 (direct, microsoft)
  • Lyo.Common.Metadata (transitive, lyo)
  • Lyo.Exceptions (transitive, lyo)
  • Microsoft.Bcl.AsyncInterfaces 10.0.5 (transitive, microsoft, netstandard2.0)
  • Microsoft.Extensions.DependencyInjection.Abstractions 10.0.5 (transitive, microsoft)
  • System.IO.Hashing 10.0.5 (transitive, microsoft, net10.0)
  • System.Memory 4.6.3 (transitive, microsoft, netstandard2.0)
  • System.Text.Json 10.0.5 (transitive, microsoft, netstandard2.0)
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Lyo.FileSystemWatcher:

Package Downloads
Lyo.FileSystemWatcher.Postgres

PostgreSQL store for FileSystemWatcher snapshots and change events. Service layer only — no HTTP.

Lyo.Drift.Agent

Drift agent: watches configured directories and posts file-tree and system-info snapshots to the collector API.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 75 9/9/2026
1.0.13 95 8/25/2026
1.0.11 90 8/23/2026
1.0.9 95 8/22/2026
1.0.6 94 8/20/2026
1.0.4 95 8/20/2026
1.0.3 90 8/19/2026
1.0.2 99 8/19/2026
1.0.1 93 8/18/2026
1.0.0 99 8/16/2026