ASP.NET Core 具有很多針對文件讀取的應用。比如我們傾向于采用JSON文件來定義配置,所以應用就會涉及針對配置文件讀取。如果用戶發送一個針對物理文件的HTTP請求,應用會根據指定的路徑讀取目標文件的內容并對請求予以響應。在一個ASP.NET Core MVC應用中,針對View的動態編譯會涉及到根據預定義的路徑映射關系來讀取目標View。這些不同應用場景都會出現一個FileProvider對象的身影,以此對象為核心的文件系統提供了統一的API來讀取文件的內容并監控內容的改變。 [ 本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、一個抽象的“文件系統”
二、呈現文件系統的結構
三、讀取物理文件內容
四、讀取內嵌于程序集中的文件內容
五、監控文件的變化
一、一個抽象的“文件系統”
本章所謂的“文件系統”有點名不副實,其實根本算不上一個系統,它僅僅是利用一個抽象化的FileProvider以統一的方式提供所需的文件而已。不過筆者實在想不到一個更為貼切的描述短語,所以還是姑且稱之為文件系統吧(github上對應的項目名稱就叫FileSystem)。作為文件系統的核心,FileProvider是對所有實現了IFileProvider接口的所有類型以及對應對象的統稱。正式因為FileProvider自身是個抽象的對象,所以由它構建的也是一個抽象的文件系統。
這個文件系統采用目錄的方式來組織和規劃文件,但是這里所謂的目錄和文件都是一個抽象的概念,并非對一個具體物理目錄和文件的映射。文件系統的目錄僅僅是文件的邏輯容器,而文件可能對應一個物理文件,也可能保存在數據庫中,或者來源于網絡,甚至有可能根本就不能存在,其內容需要在讀取時動態生成。為了讓讀者朋友們能夠對這個文件系統具有一個大體認識,我們先來演示幾個簡單的實例。
二、呈現文件系統的結構
文件系統中的文件以目錄的形式進行組織,一個FileProvider可以視為針對一個根目錄的映射。目錄除了可以存放文件之外,還可以包含多個子目錄,所以目錄/文件在整體上呈現出樹形層細化結構。接下來我們利用提供的FileProvider對象并將它映射到一個物理目錄,最終將所在目錄的整個結構呈現出來。
我們創建一個控制臺應用,并添加相應的NuGet包。由于IFileProvider接口定義在“Microsoft.Extensions.FileProviders.Abstractions”這個NuGet包中,針對物理文件的FileProvider(PhysicalFileProvider)所在的NuGet包名為“Microsoft.Extensions.FileProviders.Physical”,所以我們只需要添加后者的依賴即可。除此之外,我們將采用針對依賴注入的編程方式,我們還添加了針對“Microsoft.Extensions.DependencyInjection”這個NuGet包的依賴。如下所示的是針對這兩個NuGet包的依賴在project.json文件中的定義。
1: {
2: ...
3: "dependencies": {
4: ...
5: "Microsoft.Extensions.DependencyInjection" : "1.0.0",
6: "Microsoft.Extensions.FileProviders.Physical" : "1.0.0"
7: },
8: ...
9: }
我們定義了如下一個IFileManager接口,它利用一個唯一的方式ShowStructure將文件系統的整體結構顯示出來。該方法具有一個類型為Action<int, string>的參數,后者負責將文件系統的節點(目錄或者文件)呈現出來。對于這個Action<int, string>委托對象的兩個泛型參數,第一個整型參數代表縮進的層級,后一個代表需要顯示的目錄或者文件的名稱。
1: public interface IFileManager
2: {
3: void ShowStructure(Action<int, string> render);
4: }
如下所示的是實現了上面這個IFileManager接口的FileManager類型。構建文件系統的FileProvider對象對應著同名的只讀屬性,該屬性在構造函數中通過對應的參數進行賦值。目標文件系統的整體結構最終是通過Render方法以遞歸的方式呈現出來的,這其中涉及到FileProvider的GetDirectoryContents方法的調用。該方法返回一個DirectoryContents對象表示由指定路徑指向的目錄內容,如果對應的目錄存在,我們可以遍歷該對象得到它的子目錄和文件。目錄和文件通過一個FileInfo對象來表示,至于究竟是目錄還是文件,則通過其屬性IsDirectory來區分。
1: public class FileManager: IFileManager
2: {
3: public IFileProvider FileProvider { get; private set; }
4:
5: public FileManager(IFileProvider fileProvider)
6: {
7: this.FileProvider = fileProvider;
8: }
9:
10: public void ShowStructure (Action<int, string> render)
11: {
12: int layer = -1;
13: Render("", ref layer, render);
14: }
15:
16: private void Render(string subPath, ref int layer, Action<int, string> render)
17: {
18: layer++;
19: foreach (var fileInfo in this.FileProvider.GetDirectoryContents(subPath))
20: {
21: render(layer, fileInfo.Name);
22: if (fileInfo.IsDirectory)
23: {
24: Render($@"{subPath}\{fileInfo.Name}".TrimStart('\\'), ref layer, render);
25: }
26: }
27: layer--;
28: }
29: }
接下來我們為演示的FileProvider構建一個映射的物理目錄。將“C:\Test\”目錄作為根目錄,然后按照如下圖所示的結構在它下面創建相應的子目錄和文件。我們將利用映射為該目錄的FileProvider創建上面定義的這個FileManager,那么調用它的ShowStructure方法應該呈現出與物理目錄完全一致的結構。
我們在Main方法中編寫了如下的演示程序。我們針對目錄“C:\Test\”創建了一個PhysicalFileProvider對象,并采用服務接口類型IFileProvider注冊到ServiceCollection對象上。除此之外,注冊到同一個ServiceCollection對象上的還有IFileViwer和FileManager之間的映射。
1: new ServiceCollection()
2: .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test"))
3: .AddSingleton<IFileManager, FileManager>()
4: .BuildServiceProvider()
5: .GetService<IFileManager>()
6: .ShowStructure((layer, name) => Console.WriteLine("{0}{1}", new string('\t', layer), name));
我們最終利用ServiceCollection生成的ServiceProvider得到FileManager對象,并調用其ShowStructure方法將PhysicalFileProvider對象映射的目錄結構呈現出來。當我們運行該程序之后,控制臺上將呈現出如下所示的輸出結果,該結果為我們展示了映射物理目錄的真實結構。
三、讀取物理文件內容
上面我們演示了如何利用FileProvider將文件系統的結構完整地呈現出來,接下來我們來演示如何利用它來讀取一個具體文件的內容。我們為IFileManager定義如下一個ReadAllTextAsync方法以異步的方式讀取指定路徑對應的文件,并以字符串的形式返回讀取的內容。FileManager依然利用一個FileProvider來完成針對文件的讀取工作。具體來說,它將指定的文件路徑作為參數調用其GetFileInfo方法并得到一個FileInfo對象。接下來,我們調用FileInfo的CreateReadStream得到讀取文件的輸出流,并利用后者得到文件的真實內容,最終采用最簡單的ASCII碼轉換成返回的字符串。
1: public interface IFileManager
2: {
3: ...
4: Task<string> ReadAllTextAsync(string path);
5: }
6:
7: public class FileManager : IFileManager
8: {
9: ...
10: public async Task<string> ReadAllTextAsync(string path)
11: {
12: byte[] buffer;
13: using (Stream readStream = this.FileProvider.GetFileInfo(path).CreateReadStream())
14: {
15: buffer = new byte[readStream.Length];
16: await readStream.ReadAsync(buffer, 0, buffer.Length);
17: }
18: return Encoding.ASCII.GetString(buffer);
19: }
20: }
假設我們依然將FileManager使用的FileProvider映射為目錄“C:\Test\”,現在我們該目錄中創建一個名為data.txt的文本文件,并在該文件中任意寫入一些內容。接下來我們在Main方法中編寫了如下的程序利用依賴注入的方式得到FileManager對象,并讀取文件data.txt的內容。最終的調試斷言旨在確定通過FileProvider讀取的確實就是目標文件的真實內容。
1: string content = new ServiceCollection()
2: .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test"))
3: .AddSingleton<IFileManager, FileManager>()
4: .BuildServiceProvider()
5: .GetService<IFileManager>()
6: .ReadAllTextAsync("data.txt").Result;
7:
8: Debug.Assert(content == File.ReadAllText(@"c:\test\data.txt"));
四、讀取內嵌于程序集中的文件內容
我們一直在強調由FileProvider構建的是一個抽象的具有目錄結構的文件系統,具體文件的提供方式取決于具體FileProvider的實現。由于我們定義的FileManager并沒有限定具體使用何種類型的FileProvider,后者是在應用中通過依賴注入的方式指定的。由于上面的應用程序注入的是一個PhysicalFileProvider對象,所以我們可以利用它來讀取對應目錄下的某個文件。假設現在我們將這個hello.txt直接以資源文件的形式編譯到程序集中,我們就需要使用另一個名為EmbeddedFileProvider的FileProvider
現在我們直接將這個data.txt文件添加到控制臺應用的項目根目錄下。在默認的情況下,當我們編譯項目的時候這樣的文件并不能成為內嵌到目標程序集的資源文件,為此我們需要在project.json上作一些與編譯相關的設置。具體來說,我們需要按照如下的方式將文件hello.txt的路徑添加到通過配置節“buildOptions/embed”表示的內嵌文件列表中。除此之外,由于EmbeddedFileProvider定義在“Microsoft.Extensions.FileProviders.Embedded”這個NuGet包中,我們需要添加針對它的依賴。
1: {
2: ...
3: "buildOptions": {
4: ...
5: "embed": ["data.txt"]
6: },
7: "dependencies": {
8: ...
9: "Microsoft.Extensions.DependencyInjection" : "1.0.0",
10: "Microsoft.Extensions.FileProviders.Embedded" : "1.0.0"
11: },
12: ...
13: }
我們編寫了如下的程序來演示針對內嵌于程序集中的資源文件的讀取。我們首先得到當前入口程序集,并利用它創建了一個EmbeddedFileProvider,后者替換原來的PhysicalFileProvider對象被注冊到ServiceCollection之上。我們接下來采用與上面完全一致的編程方式得到FileManager對象并利用它讀取內嵌文件data.txt的內容。為了驗證讀取的目標文件準確無誤,我們采用直接讀取資源文件的方式得到了內嵌文件data.txt的內容,并利用一個調試斷言確定兩者的一致性。
1: Assembly assembly = Assembly.GetEntryAssembly();
2:
3: //利用EmbeddedFileProvider讀取文件
4: string content1 = new ServiceCollection()
5: .AddSingleton<IFileProvider>(new EmbeddedFileProvider(assembly))
6: .AddSingleton<IFileManager, FileManager>()
7: .BuildServiceProvider()
8: .GetService<IFileManager>()
9: .ReadAllTextAsync("data.txt").Result;
10:
11: //直接讀取內嵌資源文件
12: Stream stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.data.txt");
13: byte[] buffer = new byte[stream.Length];
14: stream.Read(buffer, 0, buffer.Length);
15: string content2 = Encoding.ASCII.GetString(buffer);
16:
17: Debug.Assert(content1 == content2);
五、監控文件的變化
在文件讀取場景中,應用數據與源文件的同步是一個很常見的需求。比如說我們將配置定義在一個JSON文件中,應用啟動的時候會讀取該文件并根據配置數據對應用作相應的設置。在很多情況下,如果我們改動了配置文件, 最新的配置數據只有在應用重啟之后才能生效。如果我們能夠以一種高效的方式對配置文件進行監控,并在其發生改變的情況下相應用發送通知,那么應用就能在不用重啟的情況下重新讀取配置文件,進而實現應用配置和原始配置文件的同步。
對文件系統試試監控并在發生改變時發送通知也是FileProvider對象的核心功能之一。接下來我們依然使用上面這個控制臺文件來演示如何使用PhysicalFileProvider來對某個物理文件試試監控,并在目標文件的內容發生改變的時候重新讀取新的內容。定義在Main方法上的整個程序代碼如下所示。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: IFileProvider fileProvider = new PhysicalFileProvider(@"c:\test");
6: ChangeToken.OnChange(() => fileProvider.Watch("data.txt"), () => LoadFileAsync(fileProvider));
7: while (true)
8: {
9: File.WriteAllText(@"c:\test\data.txt", DateTime.Now.ToString());
10: Task.Delay(5000).Wait();
11: }
12: }
13:
14: public static async void LoadFileAsync(IFileProvider fileProvider)
15: {
16: Stream stream = fileProvider.GetFileInfo("data.txt").CreateReadStream();
17: {
18: byte[] buffer = new byte[stream.Length];
19: await stream.ReadAsync(buffer, 0, buffer.Length);
20: Console.WriteLine(Encoding.ASCII.GetString(buffer));
21: }
22: }
23: }
如上面的代碼片段所示,我們針對目錄“c:\test”創建了一個PhysicalFileProvider,并調用Watch方法對指定的文件data.txt實施監控。該方法的返回類型為IChangeToken,我們正式利用這個對象接收文件改變的通知。我們調用ChangeToken的靜態方法OnChange針對這個對象注冊了一個回調,意味著當源文件發生改變的時候,注冊的回調會自動執行,進而實現對源文件的重新讀取和顯示。在程序的末端,我們以每隔5秒的間隔對文件data.txt作一次修改,而文件的內容為當前時間。所以當我們的程序啟動之后,每隔5秒鐘當前時間就會以如下的方式呈現在控制臺上。
文章列表