ASP.NET Core應用具有很多讀取文件的場景,如讀取配置文件、靜態Web資源文件(如CSS、JavaScript和圖片文件等)、MVC應用的視圖文件,以及直接編譯到程序集中的內嵌資源文件。這些文件的讀取都需要使用一個IFileProvider對象。IFileProvider對象構建了一個抽象的文件系統,我們不僅可以利用該系統提供的統一API來讀取各種類型的文件,還能及時監控目標文件的變化。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[S401] 輸出文件系統目錄結構(源代碼)
[S402]讀取物理文件內容(源代碼)
[S403]讀取內嵌文件內容(源代碼)
[S404]監控文件的變更(源代碼)
[401] 輸出文件系統目錄結構
文件系統下的文件以目錄的形式進行組織,一個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 運行程序顯示的目錄結構
[402]讀取物理文件內容
接下來我們來演示如何利用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); } } }