1. 程式人生 > >[ASP.NET Core 3框架揭祕] 檔案系統[2]:總體設計

[ASP.NET Core 3框架揭祕] 檔案系統[2]:總體設計

在《抽象的“檔案系統”》中,我們通過幾個簡單的例項演示從程式設計的角度對檔案系統做了初步的體驗,接下來我們繼續從設計的角度來進一步認識它。這個抽象的檔案系統以目錄的形式來組織檔案,我們可以利用它讀取某個檔案的內容,還可以對目錄或者檔案實施監控並及時得到變化的通知。由於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]:程式集內嵌檔案系統