ASP.NET Core应用具有很多读取文件的场景,如读取配置文件、静态Web资源文件(如CSS、JavaScript和图片文件等)、MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件。这些文件的读取都需要使用一个IFileProvider对象。IFileProvider对象构建了一个抽象的文件系统,我们不仅可以利用该系统提供的统一API来读取各种类型的文件,还能及时监控目标文件的变化。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[401] 输出文件系统目录结构[S401] 输出文件系统目录结构(源代码)
[S402]读取物理文件内容(源代码)
[S403]读取内嵌文件内容(源代码)
[S404]监控文件的变更(源代码)
文件系统下的文件以目录的形式进行组织,一个IFileProvider对象可以视为针对一个目录的映射。目录除了可以存放文件,还可以包含子目录,所以目录/文件在整体上呈现出树形层次化结构。接下来我们将一个IFileProvider对象映射到一个物理目录,并利用它将所在目录的结构呈现出来。我们创建一个控制台程序,并添加针对NuGet包“Microsoft.Extensions.FileProviders.Physical”的依赖,这个包提供了针对物理文件系统的实现。我们定义了如下一个这个IFileSystem接口,它的ShowStructure方法会将文件系统的整体结构输出到控制台上。该方法的Action<int, string>中的参数将文件系统的节点(目录或者文件)名称呈现出来,两个参数分别代表缩进的层级和目录/文件的名称。
public interface IFileSystem { void ShowStructure(Action<int, string> print); }
如下这个FileSystem类型实现了IFileSystem接口,它利用只读_fileProvider字段表示的IFileProvider对象来提取目录结构。目标文件系统的整体结构通过Print方法以递归的方式呈现出来,其中涉及对IFileProvider对象的GetDirectoryContents方法的调用,该方法返回一个表示“目录内容” 的IDirectoryContents对象。如果对应的目录存在,我们遍历所有子目录和文件。目录和文件体现为一个IFileInfo对象,至于具体是目录还是文件由 IsDirectory属性决定。
public class FileSystem : IFileSystem { private readonly IFileProvider _fileProvider; public FileSystem(IFileProvider fileProvider) => _fileProvider = fileProvider; public void ShowStructure(Action<int, string> print) { int indent = -1; Print(""); void Print(string subPath) { indent++; foreach (var fileInfo in _fileProvider.GetDirectoryContents(subPath)) { print(indent, fileInfo.Name); if (fileInfo.IsDirectory) { Print($@"{subPath}\{fileInfo.Name}".TrimStart('\\')); } } indent--; } } }
我们接下来构建一个本地物理目录“c:\test\”,并在其下面创建如图1所示子目录和文件。我们将这个目录映射到一个IFileProvider对象上,并利用后者创建的FileSystem对象将目录结构呈现出来。
图1 FileProvider映射的物理目录结构
整个演示程序体现在如下所示的代码片段中。我们针对目录“c:\test\”创建了一个表示物理文件系统的PhysicalFileProvider对象,并将其注册到创建的ServiceCollection对象上,后者还添加了针对IFileSystem/FileSystem的服务注册。
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; new ServiceCollection() .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test")) .AddSingleton<IFileSystem, FileSystem>() .BuildServiceProvider() .GetRequiredService<IFileSystem>() .ShowStructure(Print); static void Print(int layer, string name) => Console.WriteLine($"{new string(' ', layer * 4)}{name}");
我们最终利用ServiceCollection生成的IServiceProvider对象得到FileSystem对象,并调用它的ShowStructure方法将映射的目录结构呈现出来。运行该程序之后,映射物理目录的真实结构会以如图2所示形式输出到控制台上。
图2 运行程序显示的目录结构
接下来我们来演示如何利用IFileProvider对象读取一个物理文件的内容。我们为IFileSystem接口定义如下一个ReadAllTextAsync方法以异步的方式读取指定文件内容,方法的参数表示文件的路径。如下代码片段所示,ReadAllTextAsync方法将指定的文件路径作为参数来调用IFileProvider对象的GetFileInfo方法,以得到一个描述目标文件的IFileInfo对象。我们进一步调用这个IFileInfo的CreateReadStream方法得到读取文件的输出流,进而得到文件的真实内容。
public interface IFileSystem { ... Task<string> ReadAllTextAsync(string path); } public class FileSystem : IFileSystem { ... public async Task<string> ReadAllTextAsync(string path) { byte[] buffer; using (var stream = _fileProvider.GetFileInfo(path).CreateReadStream()) { buffer = new byte[stream.Length]; await stream.ReadAsync(buffer); } return Encoding.Default.GetString(buffer); } }
我们依然将IFileProvider对象映射为目录“c:\test\”,并该目录中创建一个名为data.txt的文本文件。下面的演示程序利用依赖注入容器的得到FileSystem对象,并调用其ReadAllTextAsync方法将该文件的文本内容读出来。
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using System.Diagnostics; var content = await new ServiceCollection() .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test")) .AddSingleton<IFileSystem, FileSystem>() .BuildServiceProvider() .GetRequiredService<IFileSystem>() .ReadAllTextAsync("data.txt"); Debug.Assert(content == File.ReadAllText(@"c:\test\data.txt"));[403]读取内嵌文件内容
我们一直强调IFileProvider接口代表一个抽象的文件系统,具体文件的提供方式取决于具体的实现类型。演示实例中定义的FileSystem并没有限定具体使用何种类型的IFileProvider,我们可以通过服务注册的方式指定任意实现类型。我们现在将data.txt文件直接以资源文件的形式编译到程序集中,并利用一个EmbeddedFileProvider对象来提取它的内容。EmbeddedFileProvider类型由NuGet包“Microsoft.Extensions.FileProviders.Embedded”提供,在添加了上述NuGet包的引用之后,我们直接将data.txt文件添加到控制台应用的项目根目录下。为了将该文件内嵌到编译生成的程序集中,我们可以在Visual Studio的解决方案窗口中右键选择这个文件,在打开的文件属性窗口中按照如图3所示的方式将Build Action属性设置为“Embedded resource”。
图3 设置文件的Build Action属性
上述针对内嵌文件的设置会改变项目文件(.csproj文件)的内容。具体来说,当文件的Build Action属性被设置为“Embedded resource”后,如下所示的<EmbeddedResource>节点会自动添加到项目文件中,所以我们也可以直接修改项目文件达到相同的目的。
<Project Sdk="Microsoft.NET.Sdk"> ... <ItemGroup> <EmbeddedResource Include="data.txt"/> </ItemGroup> </Project>
在如下所示的演示程序中,我们根据入口程序集创建了一个EmbeddedFileProvider对象,并用它代替原来的PhysicalFileProvider对象的服务注册。我们采用完全一致的编程方式读取内嵌文件data.txt的内容。
using App; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using System.Diagnostics; using System.Reflection; using System.Text; var assembly = Assembly.GetEntryAssembly()!; var content = await new ServiceCollection() .AddSingleton<IFileProvider>(new EmbeddedFileProvider(assembly)) .AddSingleton<IFileSystem, FileSystem>() .BuildServiceProvider() .GetRequiredService<IFileSystem>() .ReadAllTextAsync("data.txt"); var stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.data.txt"); var buffer = new byte[stream!.Length]; stream.Read(buffer, 0, buffer.Length); Debug.Assert(content == Encoding.Default.GetString(buffer));[404]监控文件的变更
确定加载到内存中的数据与源文件的一致性并自动同步是一个很常见的需求。例如,我们将配置定义在一个JSON文件中,应用启动的时候会读取该文件并将其转换成对应的Options对象。如果能够检测到文件的变换,那么配置文件被修改了之后,程序就可以自动读取新的内容并将其绑定到Options对象上。对文件系统实施监控并在其发生改变时发送通知也是IFileProvider对象提供的核心功能之一。下面的程序演示如何使用PhysicalFileProvider对某个物理文件实施监控,并在目标文件被更新时重新读取新的内容。
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; using System.Text; using var fileProvider = new PhysicalFileProvider(@"c:\test"); string? original = null; ChangeToken.OnChange(() => fileProvider.Watch("data.txt"), Callback); while (true) { File.WriteAllText(@"c:\test\data.txt", DateTime.Now.ToString()); await Task.Delay(5000); } async void Callback() { var stream = fileProvider.GetFileInfo("data.txt").CreateReadStream(); { var buffer = new byte[stream.Length]; await stream.ReadAsync(buffer); var current = Encoding.Default.GetString(buffer); if (current != original) { Console.WriteLine(original = current); } } }如上面的代码片段所示,我们针对目录“c:\test”创建了一个PhysicalFileProvider对象,并调用其Watch方法对指定的data.txt文件实施监控。该方法会利用返回的IChangeToken对象发送文件更新的通知。我们调用ChangeToken的静态方法OnChange针对这个IChangeToken对象注册了一个自动读取并显示文件内容的回调。我们每隔5秒对data.txt文件进行一次修改,并将当前时间作为文件的内容。程序启动之后,作为文件内容的当前时间每隔5秒就会以图4所示的方式输出到控制台上。
图4 实时显示监控文件的内容