.NET Core的文件系统[3]:由PhysicalFileProvider构建的物理文件系统


ASP.NET Core应用中使用得最多的还是具体的物理文件,比如配置文件、View文件以及网页上的静态文件,物理文件系统的抽象通过PhysicalFileProvider这个FileProvider来实现,该类型定义在NuGet包“Microsoft.Extensions.FileProviders.Physical”中。我们知道System.IO命名空间下定义了一整套针操作物理目录和文件的API,实际上PhysicalFileProvider最终也是通过调用这些API来完成相关的IO操作的。[ 本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、PhysicalFileProvider
二、PhysicalFileInfo
三、PhysicalDirectoryInfo
四、针对物理文件的监控
五、总结

一、PhysicalFileProvider

如下所示的代码片段展示PhysicalFileProvider类型的定义。

   1: public class PhysicalFileProvider : IFileProvider, IDisposable
   2: {   
   3:     public PhysicalFileProvider(string root);   
   4:      
   5:     public IFileInfo GetFileInfo(string subpath);  
   6:     public IDirectoryContents GetDirectoryContents(string subpath); 
   7:     public IChangeToken Watch(string filter);
   8:  
   9:     public void Dispose();   
  10: }

二、PhysicalFileInfo

一个PhysicalFileProvider对象总是映射到某个具体的物理目录下,被映射的目录所在的路径通过构造函数的参数root来提供,该目录将作为PhysicalFileProvider的根目录。GetFileInfo方法返回的FileInfo对象代表指定路径对应的文件,这是一个类型为PhysicalFileInfo的对象,如下所示的代码片段展示了该类型的完整定义。一个物理文件可以通过一个System.IO.FileInfo对象来表示,一个PhysicalFileInfo对象实际上就是对这个一个FileInfo对象的封装,定义在PhysicalFileInfo的所有属性都来源于这个FileInfo对象。对于创建读取文件输出流的CreateReadStream方法来说,它返回的是一个根据物理文件绝对路径创建的FileStream对象。

   1: public class PhysicalFileInfo : IFileInfo
   2: {
   3:     ...
   4:     public PhysicalFileInfo(FileInfo info);    
   5: }

对于PhysicalFileProvider的GetFile方法来说,即使我们指定的路径指向一个具体的物理文件,它并不总是会返回一个PhysicalFileInfo对象。具体来说,PhysicalFileProvider会将如下几种场景视为“目标文件不存在”,并让GetFile返回一个NotFoundFileInfo对象。顾名思义,NotFoundFileInfo表示的正式一个“不存在”的文件,即它的Exists属性总是返回False,而其他的属性则变得没有任何意义。当我们调用它的CreateReadStream试图读取一个根本不存在的文件内容时,会抛出一个FileNotFoundException类型的异常。

  • 确实没有一个物理文件与指定的路径相匹配。
  • 如果指定的是一个绝对路径(比如“c:\foobar”),即Path.IsPathRooted返回返回True。
  • 如果指定的路径指向一个隐藏文件。

三、PhysicalDirectoryInfo

对于PhysicalFileProvider来说,它利用PhysicalFileInfo对象来描述某个具体的物理文件,针对目录的描述则通过一个类型为PhysicalDirectoryInfo的对象。既然PhysicalFileInfo是对一个System.IO.FileInfo对象的封装,那么我们应该想得到PhysicalDirectoryInfo封装的自然就是表示目录的DirectoryInfo对象。如下面的代码片段所示,我们需要在创建一个PhysicalDirectoryInfo对象时提供这个DirectoryInfo对象,PhysicalDirectoryInfo实现的所有属性的返回值都来源于这个DirectoryInfo对象。由于CreateReadStream方法的目的是读取文件的内容,所以当我们调用一个PhysicalDirectoryInfo对象的这个方法的时候,会抛出一个InvalidOperationException类型的异常。

   1: public class PhysicalDirectoryInfo : IFileInfo
   2: {   
   3:     ...
   4:     public PhysicalDirectoryInfo(DirectoryInfo info);
   5: }

当我们调用PhysicalFileProvider的GetDirectoryContents方法时,如果指定的路径指向一个具体的目录,那么该方法会返回一个类型为EnumerableDirectoryContents的对象,不过EnumerableDirectoryContents仅仅是一个在编程过程中不可见的内部类型。EnumerableDirectoryContents是一个FileInfo对象的集合,该集合中会包括所有描述子目录的PhysicalDirectoryInfo对象和描述文件的PhysicalFileInfo对象。至于EnumerableDirectoryContents的Exists属性,它总是返回True。如果指定的路径并不指向一个存在目录,或者指定的是一个绝对路径,这个方法都会返回一个Exsits属性总是返回False的NotFoundDirectoryContents对象。

四、针对物理文件的监控

我们接着来谈谈PhysicalFileProvider的Watch方法。当我们调用该方法的时候,PhysicalFileProvider会通过解析我们提供的筛选表达式确定我们期望监控的文件,然后利用FileSystemWatcher对象来对这些文件试试监控。针对这些文件的变化(创建、修改、重命名和删除)都会实时地反映到Watch方法返回的ChangeToken上。 值得一提的是,FileSystemWatcher类型实现IDisposable接口,PhysicalFileProvider也实现了相同的接口,PhysicalFileProvider的Dispose方法的唯一使命就是释放这个FileSystemWatcher对象

Watch方法中指定的筛选表达式必须是针对当前PhysicalFileProvider根目录的相对路径,可以使用“/”或者“./”前缀,也可以不采用任何前缀。一旦我们使用了绝对路径(比如“c:\test\*.txt”)或者“../”前缀(比如“../test/*.txt”),不论解析出来的文件是否存在于PhysicalFileProvider的根目录下,这些文件都不会被监控。除此之外,如果我们没有指定任何筛选条件,也不会有任何的文件会被监控。

监控文件变化的真正目的在于让应用程序能够及时感知到数据源的改变,进而自动执行某些预先注册的回掉操作。回调的注册可以直接通过调用ChangeToken的RegisterChangeCallback方法来完成,注册的回调通过一个类型为Action的委托对象来表示。对于在第一节演示的文件监控的实例,相应的程序“照理说”可以改写成如下的形式。

   1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
   2: fileProvider.Watch("data.txt").RegisterChangeCallback(_ = >LoadFileAsync(fileProvider), null);
   3: while (true)
   4: {
   5:     File.WriteAllText(@"c:\test\data.txt", DateTime.Now.ToString());
   6:     Task.Delay(5000).Wait();
   7: }
   8:  
   9: public static async void LoadFileAsync(IFileProvider fileProvider)
  10: {
  11:     Stream stream = fileProvider.GetFileInfo("data.txt").CreateReadStream();
  12:     {
  13:         byte[] buffer = new byte[stream.Length];
  14:         await stream.ReadAsync(buffer, 0, buffer.Length);
  15:         Console.WriteLine(Encoding.ASCII.GetString(buffer));
  16:     }
  17: }

如果执行上面这段程序,我们会发现只有第一个针对文件的更新能够被感知,后续的文件更新操作将自动被忽略。导致这个问题的根源在于,单个ChangeToken对象的使命在于当绑定的数据源第一次发生变换时对外发送相应的信号,而不具有持续发送数据变换的能力。其实这一点从IChangeToken接口的定义就可以看出来,我们知道它具有一个HasChanged属性表示数据是否已经发生变化,而并没有提供一个让这个属性“复位”的方法。所以当我们需要对某个文件进行持续监控的时候,我们需要在注册的回调中重新调用FileProvider的Watch方法,并利用生成ChangeToken再次注册回调。除此之外,考虑到ChangeToken的RegisterChangeCallback方法以一个IDisposable对象的形式返回回调注册对象,我们应该在对回调实施二次注册时调用第一次返回的回调注册对象的Dispose方法将其释放掉。如下所示的程序才能达到对文件试试持续监控的目的。

   1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
   2: Action<object> callback = null;
   3: IDisposable regiser = null;
   4: callback = _ =>
   5: {
   6:     regiser.Dispose();
   7:     LoadFileAsync(fileProvider);
   8:     fileProvider.Watch("data.txt").RegisterChangeCallback(callback, null);
   9: };
  10:  
  11: regiser = fileProvider.Watch("data.txt").RegisterChangeCallback(callback, null);

不过这样的编程方式不但看起来比较繁琐,很多对ChangeToken缺乏认识的人甚至对这样的编程方式无法理解。为了解决这个问题,我们可以使用定义在ChangeToken类型中如下两个方法OnChange方法来注册数据发生改变时自动执行的回调。这两个方法具有两个参数,前者是一个用于创建ChangeToken对象的Func对象,后者则是代表回调操作的Action/Action对象。实际上第一节的实例演示中我们就是调用的这个OnChange方法。

   1: public static class ChangeToken
   2: {
   3:     public static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer)
   4:     {        
   5:         Action<object> callback = null;
   6:         callback = delegate (object s) {
   7:             changeTokenConsumer();
   8:             changeTokenProducer().RegisterChangeCallback(callback, null);
   9:         };
  10:         return changeTokenProducer().RegisterChangeCallback(callback, null);
  11:     }
  12:  
  13:     public static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer, TState state)
  14:     {
  15:         Action<object> callback = null;
  16:         callback = delegate (object s) {
  17:             changeTokenConsumer((TState) s);
  18:             changeTokenProducer().RegisterChangeCallback(callback, s);
  19:         };
  20:         return changeTokenProducer().RegisterChangeCallback(callback, state);
  21:     }
  22: }

如果改用这个OnChange方法来替换掉原来手工调用ChangeToken的RegisterChangeCallback方法进行回调注册的方式,原本显得相对繁琐的程序可以通过如下两句代码来替换。实际上在《读取并监控文件的变化》中,我们调用的正是这个OnChange方法。

   1: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
   2: ChangeToken.OnChange(() => fileProvider.Watch("data.txt"), () => LoadFileAsync(fileProvider));

五、总结

我们借助下图所示的UML来对由PhysicalFileProvider构建物理文件系统的整体设计做一个简单的总结。首先,该文件系统下用于描述目录和文件的分别是一个PhysicalDirectoryInfo和PhysicalFileInfo对象,它们分别是对一个DirectoryInfo和FileInfo(System.IO.FileInfo)对象的封装。PhysicalFileProvider的GetDirectoryContents方法返回一个EnumerableDirectoryContents对象(如果指定的目录存在),组成该对象的分别是根据其所有子目录和文件创建的PhysicalDirectoryInfo和PhysicalFileInfo对象。当我们调用PhysicalFileProvider的GetFileInfo方法时,如果指定的文件存在,返回的是描述该文件的PhysicalFileInfo对象。至于PhysicalFileProvider的Watch方法,它最终利用了FileSystemWatcher来监控指定文件的变化。