May 5, 2022

IWebHostEnvironment MapPath Extension

Brings MapPath() to .NET Core

public static class IWebHostEnvironmentExtender
{
  public static string MapPath(
    this IWebHostEnvironment self, string path)
  {
    var res = path;
    var ix = path.IndexOf('~');
    if (ix == 0)
    {
      res = path.Replace("~", self.WebRootPath);
    }
    return res;
  }
}

FileSystemWatcher Extensions

We can use extension classes to allow React to be used with other FileSystemWatcher usage cases:
public static class FileSystemWatcherExtender
{
  // Returns an IObservable, which is effectively a publisher
  // which can be subscribed to and disposed of when finished with
  public static IObservable<FileSystemEventArgs> 
    GetFileChangedPublisher(this FileSystemWatcher fileSystemWatcher)
  {
      Debug.Assert(fileSystemWatcher != null);
      var result = Observable
        .FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
            ev => fileSystemWatcher.Changed += ev,
            ev => fileSystemWatcher.Changed -= ev)
        .Select(ev => ev.EventArgs);
      return result;
  }

  // Use this to subscribe to FileSystemWatcher errors
  public static IObservable<ErrorEventArgs> 
    GetErrorPublisher(this FileSystemWatcher fileSystemWatcher)
  {
    Debug.Assert(fileSystemWatcher != null);
    return Observable
      .FromEventPattern<ErrorEventHandler, ErrorEventArgs>(
            ev => fileSystemWatcher.Error += ev,
            ev => fileSystemWatcher.Error -= ev)
      .Select(x => x.EventArgs);
  }
}
And to use it
FileSystemWatcher filesystemwatcher = BuildMyFileSystemWatcher();
...
// Subscribed to filesystem watcher changes
// where "onChanged" is a Func<FileSystemEventArgs>
IDisposable subscription = fileSystemWatcher.GetFileChangedPublisher().Subscribe(onChanged);   
...
subscription.Dispose(); // Finish with our subscription
...

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.

May 1, 2022

Logging Assertions in ASP .Net Core

Sometimes it useful to extend the logging system so that you can log assertions/code contracts. Simple logger add-on so that assertions/code contracts can be logged in ASP.Net:
using System.Runtime.CompilerServices;

public static class ILoggerAssertionExtender
{
  public static bool Assert(this ILogger logger, 
    Func<bool> predicate, 
    Func<string> message)
  {
      bool assertion = predicate();
      if (!assertion)
      {
          logger.LogError("Code contract/assertion failed :" + message());
      }
      // The assertion outcome is returned so the user can react to it (throw an exception, 
      // insert a DebuggerBreak(), ...) after the logging is done.
      return assertion; 
  }
  
  // This one adds Caller attributes to get more details
  public static bool CallerAssert(this ILogger logger, 
    Func<bool> predicate,
    Func<string> message,
    [CallerMemberName] string member = "", 
    [CallerFilePath] string file = "", 
    [CallerLineNumber] int line = -1)
  {
      bool assertion = predicate();
      if (!assertion)
      {
          logger.LogError($"Code contract/assertion failed in Member {member}, File {file}, Line {line}: " + message());
      }
      // The assertion outcome is returned so the user can react to it (throw an exception, 
      // insert a DebuggerBreak(), ...) after the logging is done.
      return assertion; 
  }
}