May 4, 2022

A File Change Monitor Class in C#

Wrote this File changes monitor service to monitor 1 or more files in a particular directory for file write accesses.
public class FileChangedEvent
{
    public FileChangedEvent(string fullPath, string fileName, 
      WatcherChangeTypes changeType)
    {
        FullPath = fullPath;
        FileName = fileName;
        ChangeType = changeType;
    }

    // Full file path and name
    public string FullPath { get; private set; }
    
    // File name only
    public string FileName { get; private set; }
    
    // The type of change
    public WatcherChangeTypes ChangeType { get; private set; }
}

public interface IFileChangeMonitor
{
    void Dispose();
    string DirectoryPath { get; }
    void Start();
    public bool IsActive { get; }
    void Stop();
    IDisposable SubscribeOnChanged(string fileName, 
      Action<FileChangedEvent> onChanged, double throttle_ms = 100);
    IDisposable SubscribeOnError(Action<ErrorEventArgs> onError);
}

public class FileChangeMonitor : IDisposable, IFileChangeMonitor
{
    // https://weblogs.asp.net/ashben/31773 
    private readonly FileSystemWatcher _fileSystemWatcher; 
    private readonly DirectoryInfo _directory; // directory being monitored
    private const string DefaultFileFilter = @"*.json";

    // TODO: add NotifyFilter parameter
    public FileChangeMonitor(DirectoryInfo dir, 
      string defaultFileFilter = DefaultFileFilter)
    {
        //const int MAX_NUM_FILES_TO_MONITOR = 10;
        //const int AVG_FILE_NAME_LENGTH = 30;
        //const int FOUR_KB = 1024 * 4;
        Debug.Assert(dir != null);
        Debug.Assert(dir.Exists == true);

        _directory = new DirectoryInfo(dir.FullName);
        //int bufferSize = ((MAX_NUM_FILES_TO_MONITOR * 
        // (16 + AVG_FILE_NAME_LENGTH * 2) % FOUR_KB) + 1) * FOUR_KB;
        _fileSystemWatcher = new FileSystemWatcher(_directory.FullName)
        {
            //InternalBufferSize = bufferSize, default is enough
            NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite,
            Filter = defaultFileFilter,
            IncludeSubdirectories = false
        };
    }

    public string DirectoryPath => _directory?.FullName;

    public IDisposable SubscribeOnChanged(string fileName, 
      Action<FileChangedEvent> onChanged, 
      double throttle_ms = 200)
    {
        Debug.Assert(_directory?.Exists == true);
        Trace.WriteLine($"*** Subscribing for changes to 
  {Path.Combine(DirectoryPath, fileName)}");
        return GetFileChangedPublisher(fileName, throttle_ms)
          .Subscribe(onChanged);
    }

    public void Start()
    {
        Debug.Assert(_directory?.Exists == true);
        _fileSystemWatcher.EnableRaisingEvents = true;
    }

    public bool IsActive => _fileSystemWatcher.EnableRaisingEvents;

    public void Stop()
    {
        Debug.Assert(_directory?.Exists == true);
        _fileSystemWatcher.EnableRaisingEvents = false;
    }

    public void Dispose()
    {
        if (IsActive)
            Stop();
        GC.SuppressFinalize(this);
        _fileSystemWatcher.Dispose();
    }

    // Returns an IObservable, which is effectively a publisher which
    // can be subscribed to and disposed of when finished with
    private IObservable<FileChangedEvent> GetFileChangedPublisher(
      string targetFileName, 
      double throttle_ms = 200)
    {
        Debug.Assert(_directory?.Exists == true);

        IObservable<EventPattern<FileSystemEventArgs>> fswCreated = 
          Observable.FromEventPattern<FileSystemEventArgs>(_fileSystemWatcher, "Changed");

        return fswCreated.
               Where((EventPattern<FileSystemEventArgs> ev) => ev.EventArgs.Name == targetFileName).
               // Rx Extension Sample() will only allow the last event within a time
               // interval to pass through. This effectively throttles events
               Sample(TimeSpan.FromMilliseconds(throttle_ms)).
               Select(ev => new FileChangedEvent(ev.EventArgs.FullPath, ev.EventArgs.Name, ev.EventArgs.ChangeType)); 
    }

    private IObservable<ErrorEventArgs> GetErrorPublisher()
    {
        Debug.Assert(_directory?.Exists == true);

        return Observable
          .FromEventPattern<ErrorEventHandler, ErrorEventArgs>(
                   ev => _fileSystemWatcher.Error += ev,
                   ev => _fileSystemWatcher.Error -= ev)
          .Select(x => x.EventArgs);
    }

    public IDisposable SubscribeOnError(Action<ErrorEventArgs> onError)
    {
        Debug.Assert(_directory?.Exists == true);
        return GetErrorPublisher().Subscribe(onError);
    }
}

The thing is it is an example of how to integrate React with a file system watcher or an existing class that publishes events using standard C# events. Here is an example of how to use the service

DirectoryInfo dir = new DirectoryInfo("\\Some\Directory");
FileChangeMonitor datafileMonitor = new FileChangeMonitor(dir);
datafileMonitor.Start();
...
// Subscribed to just 1 file but we could subscribe to multiple in the same directory
IDisposable subscription = datafileMonitor.SubscribeOnChanged(
    "FileToMonitor.json",
    fce => DoSomethingWith(fce));   
...
subscription.Dispose(); // Finish with our subscription to a specific file's changes
...
datafileMonitor.Stop(); // Optionally we could just call just Dispose()
datafileMonitor.Dispose();

We could change the class so that the type of changes monitored could be passed in through the class constructor (the NotifyFilter enumeration). Also, an option to change the internal buffer size could be required.

No comments: