5.1 抽象的文件系统
IFileProvider对象可以构建一个具有层次化目录结构的文件系统。由于 IFileProvider是一个接口,所以它构建的是一个抽象的文件系统,这里所谓的目录和文件都是一个抽象的概念。具体的文件可能对应一个物理文件,也可能保存在数据库中,或者来源于网络,甚至有可能根本就不存在,其内容需要在读取时动态生成。目录也仅仅是组织文件的逻辑容器。为了使读者对这个文件系统有一个大体认识,下面先演示几个简单的实例。
5.1.1 树形层次结构
文件系统管理的所有文件以目录的形式进行组织,一个 IFileProvider 对象可以视为针对一个根目录的映射。目录除了可以存放文件,还可以包含子目录,所以目录/文件在整体上呈现出树形层次化结构。接下来我们将一个 IFileProvider 对象映射到一个物理目录,并利用它将所在目录的结构呈现出来。
下面的演示实例是一个普通的控制台程序。我们在演示实例中定义了如下一个IFileManager 接口,它利用 ShowStructure 方法将文件系统的整体结构显示出来。该方法的Action<int,string>中的参数可以将文件系统的节点(目录或者文件)名称呈现出来。这个Action<int,string>对象的两个参数分别代表缩进的层级和目录/文件的名称。
我们定义如下这个 FileManager 类,作为对 IFileManager 接口的默认实现,它利用只读_fileProvider 字段表示的 IFileProvider 对象来提取目录结构。目标文件系统的整体结构通过Render方法以递归的方式呈现出来,其中涉及对 IFileProvider对象的 GetDirectoryContents方法的调用。该方法返回一个 IDirectoryContents 对象,以表示指定目录的内容,如果对应的目录存在,就可以遍历该对象得到它的子目录和文件。目录和文件最终体现为一个 IFileInfo 对象,而IFileInfo对象对应的是一个目录还是一个文件,则通过其IsDirectory属性进行区分。
接下来构建一个本地物理目录“c:\test\”,并按照图 5-1 中的结构在其下面创建相应的子目录和文件。我们会将这个目录映射到一个 IFileProvider 对象上,并进一步利用它创建上面的FileManager对象。最终调用FileManager对象的ShowStructure方法将目录结构呈现出来。
图5-1 FileProvider映射的物理目录结构
整个演示程序体现在如下所示的代码片段中。我们针对目录“c:\test\”创建了一个表示物理文件系统的PhysicalFileProvider对象,并将其注册到创建的ServiceCollection对象上。除此之外,ServiceCollection对象上还添加了针对IFileManager/FileManager的服务注册。
我们最终利用ServiceCollection生成的IServiceProvider对象得到FileManager对象,并调用该对象的 ShowStructure方法将 PhysicalFileProvider对象映射的目录结构呈现出来。运行该程序之后,控制台上输出的结果如图5-2所示,该结果展示了映射物理目录的真实结构。(S501)
图5-2 运行程序显示的目录结构
5.1.2 读取文件内容
前面演示了如何利用 IFileProvider 对象将文件系统的结构完整地呈现出来,接下来我们将演示如何利用它来读取一个物理文件的内容。我们为 IFileManager 定义一个 ReadAllTextAsync方法,以异步的方式读取指定文件内容,方法的参数表示文件的路径。如下面的代码片段所示,ReadAllTextAsync 方法将指定的文件路径作为参数来调用 IFileProvider 对象的 GetFileInfo 方法,以得到一个 IFileInfo对象。最终调用 IFileInfo对象的 CreateReadStream方法得到读取文件的输出流,进而得到文件的真实内容。
如果依然将FileManager使用的IFileProvider映射为目录“c:\test\”,现在我们就在该目录中创建一个名为 data.txt的文本文件,并在该文件中任意写入一些内容。然后在 Main 方法中编写如下程序,利用依赖注入的方式得到 FileManager对象,并读取文件 data.txt的内容。最终的调试断言旨在确定通过IFileProvider读取的确实就是目标文件的真实内容。(S502)
我们一直强调,IFileProvider 对象构建的是一个抽象的具有目录结构的文件系统,具体文件的提供方式取决于具体的 IFileProvider对象的类型。演示实例中定义的 FileManager并没有限定具体使用何种类型的IFileProvider,该对象是在应用中通过依赖注入的方式指定的。由于上面的应用程序注入的是一个 PhysicalFileProvider 对象,所以可以利用它读取对应物理目录下的某个文件。如果将 data.txt 文件直接以资源文件的形式编译到程序集中,就需要使用另一个名为EmbeddedFileProvider的实现类型。
可以直接将 data.txt文件添加到控制台应用的项目根目录下。在默认情况下,编译项目的时候这样的文件并不能成为内嵌到目标程序集的资源文件,为此我们需要修改项目文件(.csproj文件)的内容。具体来说,我们可以在项目文件中按照如下形式添加一个<EmbeddedResource>元素,从而将data.txt文件设置为内嵌到编译后生成的程序集的内嵌资源文件中。
如下程序可以演示针对内嵌于程序集中的资源文件的读取。我们首先得到当前入口程序集,并利用它创建了一个EmbeddedFileProvider对象,用于代替原来的PhysicalFileProvider对象来被注册到 ServiceCollection之中。然后采用完全一致的编程方式得到 FileManager对象,并利用它读取内嵌文件 data.txt的内容。为了验证读取的目标文件准确无误,可以采用直接读取资源文件的方式得到内嵌文件data.txt的内容,并利用一个调试断言确定两者的一致性。(S503)
5.1.3 监控文件的变化
在文件读取场景中,确定加载到内存中的数据与源文件的一致性并自动同步,是一个很常见的需求。例如,可以将配置定义在一个 JSON 文件中,应用启动的时候会读取该文件并将其转换成对应的Options对象。在很多情况下,如果改动了配置文件,最新的配置数据只有在应用重启之后才能生效。如果能够以一种高效的方式对配置文件进行监控,并在其发生改变的情况下向应用发送通知,那么应用就能在不用重启的情况下重新读取配置文件,进而实现Options对象承载的内容和原始配置文件完全同步。
对文件系统实施监控并在其发生改变时发送通知也是 IFileProvider 对象提供的核心功能之一。下面依然使用前面这个程序来演示如何使用 PhysicalFileProvider 对某个物理文件实施监控,并在目标文件的内容发生改变时重新读取新的内容。
如上面的代码片段所示,我们针对目录“c:\test”创建了一个 PhysicalFileProvider 对象,并调用其Watch方法对指定的data.txt文件实施监控。该方法会返回一个IChangeToken对象,我们正是利用这个对象来接收文件改变通知的。我们调用ChangeToken的静态方法OnChange,针对这个对象注册了一个回调,用于实现对源文件的重新读取和显示,当源文件发生改变时,注册的回调会自动执行。我们每隔5秒对data.txt文件进行一次修改,而文件的内容为当前时间。所以,程序启动之后,每隔5秒当前时间就会以图5-3所示的方式呈现在控制台上。(S504)
图5-3 实时显示监控文件的内容