ASP.NET Core應用中使用得最多的還是具體的物理文件,比如配置文件、View文件以及網頁上的靜態文件,物理文件系統的抽象通過PhysicalFileProvider這個FileProvider來實現,該類型定義在NuGet包“Microsoft.Extensions.FileProvider... ...
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<object>的委托對象來表示。對於在第一節演示的文件監控的實例,相應的程式“照理說”可以改寫成如下的形式。
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<IChangeToken>對象,後者則是代表回調操作的Action<object>/Action<TState>對象。實際上第一節的實例演示中我們就是調用的這個OnChange方法。
1: public static class ChangeToken
2: {
3: public static IDisposable OnChange(Func<IChangeToken> 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<TState>(Func<IChangeToken> changeTokenProducer, Action<TState> 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來監控指定文件的變化。