在《抽象的“文件系統”》中,我們通過幾個簡單的實例演示從編程的角度對文件系統做了初步的體驗,接下來我們繼續從設計的角度來進一步認識它。這個抽象的文件系統以目錄的形式來組織文件,我們可以利用它讀取某個文件的內容,還可以對目錄或者文件實施監控並及時得到變化的通知。由於IFileProvider對象提供了針對文件系統變換的監控功能,在.NET Core下里類似的功能大都利用一個IChangeToken對象來實現,所以我們在對IFileProvider進行深入介紹之前有必要先來了解一下IChangeToken。
一、IChangeToken
從字面上理解的IChangeToken對象就是一個與某組監控數據關聯的“令牌(Token)”,它能夠在檢測到數據改變的時候及時地對外發出一個通知。如果IChangeToken關聯的數據發生改變,它的HasChanged屬性將變成True。我們可以調用其RegisterChangeCallback方法注冊一個在數據發生改變時可以自動執行的回調,該方法會返回一個IDisposable對象,我們通過用其Dispose方法解除注冊的回調。至於IChangeToken接口的另一個屬性ActiveChangeCallbacks,它表示當數據發生變化時是否需要主動執行注冊的回調操作。
public interface IChangeToken { bool HasChanged { get; } bool ActiveChangeCallbacks { get; } IDisposable RegisterChangeCallback(Action<object> callback, object state); }
.NET Core提供了若干原生的IChangeToken實現類型,我們最常使用的是一個名為CancellationChangeToken的實現。CancellationChangeToken的實現原理很簡單,它基本上就是按照如下的形式借助我們熟悉的CancellationToken對象來發送通知。
public class CancellationChangeToken : IChangeToken { private readonly CancellationToken _token; public CancellationChangeToken(CancellationToken token) => _token = token; public bool HasChanged => _token.IsCancellationRequested; public bool ActiveChangeCallbacks => true; public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _token.Register(callback, state); }
除了CancellationChangeToken,有時也我們也會使用到一個名為CompositeChangeToken的實現。顧名思義,CompositeChangeToken代表由多個IChangeToken組合而成的復合型IChangeToken對象。如下面的代碼片段所示,我們在調用構造函數創建一個CompositeChangeToken對象的時候,需要提供這些IChangeToken對象。對於一個CompositeChangeToken對象來說,只要組成它的任何一個IChangeToken發生改變,其HasChanged屬性將會變成True,而注冊的回調自然會被執行。至於ActiveChangeCallbacks屬性,只要任何一個IChangeToken的同名屬性返回True,該屬性就會返回True。
public class CompositeChangeToken : IChangeToken { public bool ActiveChangeCallbacks { get; } public IReadOnlyList<IChangeToken> ChangeTokens { get; } public bool HasChanged { get; } public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens); public IDisposable RegisterChangeCallback(Action<object> callback, object state); }
我們可以直接調用IChangeToken提供的RegisterChangeCallback方法來注冊在接收到數據變化通知后的回調操作,但是更常用的方式則是直接調用靜態類型ChangeToken提供的如下兩個OnChange方法重載來進行回調注冊,這兩個方法的第一個參數需要被指定為一個用來提供IChangeToken對象的Func<IChangeToken>委托。
public static class ChangeToken { public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer) ; public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state) ; }
二、IFileProvider
在了解了IChangeToken是怎樣一個對象之后,我們將關注轉移到文件系統的核心接口IFileProvider上,該接口定義在NuGet包“Microsoft.Extensions.FileProviders.Abstractions”中。我們在《抽象的“文件系統”》做了幾個簡單的實例演示,它們實際上體現了文件系統承載的三個基本功能,而這三個基本功能分別體現在IFileProvider接口如下所示的三個方法中。
public interface IFileProvider { IFileInfo GetFileInfo(string subpath); IDirectoryContents GetDirectoryContents(string subpath); IChangeToken Watch(string filter); }
三、IFileInfo
雖然文件系統采用目錄來組織文件,但是不論是目錄還是文件都通過一個IFileInfo對象來表示,至於具體是目錄還是文件則通過IFileInfo的IsDirectory屬性來確定。對於一個IFileInfo對象,我們可以通過只讀屬性Exists判斷指定的目錄或者文件是否真實存在。至於另外兩個屬性Name和PhysicalPath,它們分別表示文件或者目錄的名稱和物理路徑。屬性LastModified返回一個時間戳,表示目錄或者文件最終一次被修改的時間。對於一個表示具體文件的IFileInfo對象來說,我們可以利用屬性Length得到文件內容的字節長度。如果我們希望讀取文件的內容,可以借助於CreateReadStream方法返回的Stream對象來完成。
public interface IFileInfo { bool Exists { get; } bool IsDirectory { get; } string Name { get; } string PhysicalPath { get; } DateTimeOffset LastModified { get; } long Length { get; } Stream CreateReadStream(); }
IFileProvider接口的GetFileInfo方法會根據指定的路徑得到表示所在文件的IFileInfo對象。換句話說,雖然一個IFileInfo對象可以用於描述目錄和文件,但是GetFileInfo方法的目的在於得到指定路徑返回的文件而不是目錄(我個人不太認同這種令人產生歧義的API設計)。一般來說,不論指定的文件是否存在,該方法總會返回一個具體的IFileInfo對象,因為目標文件的存在與否是由該對象的Exists屬性來確定的。
四、IDirectoryContents
如果希望得到某個目錄的內容,比如需要查看多少文件或者子目錄包含在這個目錄下,我們可以調用IFileProvider對象的GetDirectoryContents方法並將所在目錄的路徑作為參數。目錄內容通過該方法返回的IDirectoryContents對象來表示。如下面的代碼片段所示,一個IDirectoryContents對象實際上是一組IFileInfo對象的集合,組成這個集合的所有IFileInfo自然就是對包含在這個目錄下的所有文件和子目錄的描述。和GetFileInfo方法一樣,不論指定的目錄是否存在,GetDirectoryContents方法總是會返回一個具體的IDirectoryContents對象,它的Exists屬性會幫助我們確定指定目錄是否存在。
public interface IDirectoryContents : IEnumerable<IFileInfo> { bool Exists { get; } }
五、監控目錄或者文件更新
如果我們希望監控IFileProvider所在目錄或者文件的變化,我們可以調用它的Watch方法,當然前提是對應的IFileProvider對象提供了這樣的監控功能。這個方法接受一個字符串類型的參數filter,我們可以利用這個參數指定一個針對“文件匹配模式(File Globing Pattern)”表達式(以下簡稱Globing Pattern表達式)來篩選需要監控的目標目錄或文件。
Globing Pattern表達式比正則表達式簡單多了,它只包含“*”一種“通配符”,如果硬說它包含兩種通配符的話,那么另一個通配符是“**”。Globing Pattern表達式體現為一個文件路徑,其中“*”代表所有不包括路徑分隔符(“/”或者“\”)的所有字符,而“**”則代表包含路徑分隔符在內的所有字符。下表給出了幾個典型的Globing Pattern表達式和它們代碼的文件匹配語義。
Globing Pattern表達式 |
匹配的文件 |
src/foobar/foo/settings.*
|
子目錄“src/foobar/foo/”(不含其子目錄)下名為“settings”的所有文件,比如settings.json、settings.xml和settings.ini等。 |
src/foobar/foo/*.cs
|
子目錄“src/foobar/foo/”(不含其子目錄)下的所有.cs文件。 |
src/foobar/foo/*.* |
子目錄“src/foobar/foo/”(不含其子目錄)下所有文件。 |
src/**/*.cs |
子目錄“src”(含其子目錄)下的所有.cs文件。 |
一般來說,不論是調用IFileProvider對象的GetFileInfo或GetDirectoryContents方法所指定的目標文件或目錄的路徑,還是調用Watch方法指定的篩選表達式,都是一個針對當前IFileProvider對象映射根目錄的相對路徑。指定的這個路徑可以采用“/”字符作為前綴,但是這個前綴是不必要的。換句話說,如下所示的這兩組程序是完全等效的。
路徑不包含前綴“/”
var dirContents = fileProvider.GetDirectoryContents("foobar"); var fileInfo = fileProvider.GetFileInfo("foobar/foobar.txt"); var changeToken = fileProvider.Watch("foobar/*.txt");
路徑包含前綴“/”
var dirContents = fileProvider.GetDirectoryContents("/foobar"); var fileInfo = fileProvider.GetFileInfo("/foobar/foobar.txt"); var changeToken = fileProvider.Watch("/foobar/*.txt");
總的來說,以IFileProvider對象為核心的文件系統在設計上看是非常簡單的。除了IFileProvider接口之外,文件系統還涉及到其他一些對象,比如IDirectoryContents、IFileInfo和IChangeToken等,下圖所示的UML展示了這些接口以及它們之間的關系。
[ASP.NET Core 3框架揭秘] 文件系統[1]:抽象的“文件系統”
[ASP.NET Core 3框架揭秘] 文件系統[2]:總體設計
[ASP.NET Core 3框架揭秘] 文件系統[3]:物理文件系統
[ASP.NET Core 3框架揭秘] 文件系統[4]:程序集內嵌文件系統