1. 程式人生 > >[ASP.NET Core 3框架揭祕] 檔案系統[1]:抽象的“檔案系統”

[ASP.NET Core 3框架揭祕] 檔案系統[1]:抽象的“檔案系統”

ASP.NET Core應用 具有很多讀取檔案的場景,比如配置檔案、靜態Web資原始檔(比如CSS、JavaScript和圖片檔案等)以及MVC應用的View檔案,甚至是直接編譯到程式集中的內嵌資原始檔。這些檔案的讀取都需要使用到一個IFileProvider物件。IFileProvider物件構建了一個抽象的檔案系統,我們不僅可以利用它提供的統一API來讀取各種型別的檔案,還能及時監控目標檔案的變化。

一、樹形層次結構

IFileProvider物件為我們構建了一個具有層次化目錄結構的檔案系統。由於IFileProvider是一個介面,所以由它構建的是一個抽象化的檔案系統,這裡所謂的目錄和檔案都是一個抽象的概念。具體的檔案可能對應一個物理檔案,也可能儲存在資料庫中,或者來源於網路,甚至有可能根本就不存在,其內容需要在讀取時動態生成。目錄也僅僅是組織檔案的邏輯容器。為了讓讀者朋友們對這個檔案系統有一個大體認識,我們先來演示幾個簡單的例項。

檔案系統管理的所有檔案以目錄的形式進行組織,一個IFileProvider物件可以視為針對一個根目錄的對映。目錄除了可以存放檔案之外,還可以包含子目錄,所以目錄/檔案在整體上呈現出樹形化層次化結構。接下來我們將一個IFileProvider物件對映到一個物理目錄,並利用它將所在目錄的結構呈現出來。

我們演示例項是一個普通的控制檯程式。我們在演示例項中定義瞭如下一個IFileManager介面,它利用一個唯一的ShowStructure方法將檔案系統的整體結構顯示出來。該方法具有一個型別為Action<int, string>的引數負責將檔案系統的節點(目錄或者檔案)名稱呈現出來。這個Action<int, string>物件的兩個引數分別代表縮排的層級和目錄/檔案的名稱。

public interface IFileManager
{
    void ShowStructure(Action<int, string> render);
}

我們定義如下這個FileManager類作為對IFileManager介面的預設實現,它利用只讀_fileProvider欄位表示的IFileProvider物件來提取目錄結構。目標檔案系統的整體結構通過Render方法以遞迴的方式呈現出來,其中涉及到對IFileProvider物件的GetDirectoryContents方法的呼叫。該方法返回一個IDirectoryContents物件表示指定目錄的內容,如果對應的目錄存在,我們可以遍歷該物件得到它的子目錄和檔案。目錄和檔案最終體現為一個IFileInfo物件來,至於IFileInfo物件對應的就是一個目錄還是一個檔案,則通過其IsDirectory屬性來區分。

public class FileManager : IFileManager
{
    private readonly IFileProvider _fileProvider;
    public FileManager(IFileProvider fileProvider) => _fileProvider = fileProvider;
    public void ShowStructure(Action<int, string> render)
    {
        int indent = -1;
        Render("");
        void Render(string subPath)
        {
            indent++;
            foreach (var fileInfo in _fileProvider.GetDirectoryContents(subPath))
            {
                render(indent, fileInfo.Name);
                if (fileInfo.IsDirectory)
                {
                    Render($@"{subPath}\{fileInfo.Name}".TrimStart('\\'));
                }
            }
            indent--;
        }
    }        
}

接下來我們構建一個本地物理目錄“c:\test\”,並按照如下圖所示的結構在它下面建立相應的子目錄和檔案。我們會將這個目錄對映到一個IFileProvider物件上,並進一步利用它創建出上面這個FileManager物件。我們最終呼叫這個FileManager物件的ShowStructure方法將目錄結構呈現出來。

整個演示程式體現在如下的程式碼片段中。我們針對目錄“c:\test\”建立了一個表示物理檔案系統的PhysicalFileProvider物件,並將其註冊到建立的ServiceCollection物件上。除此之外,ServiceCollection物件上還添加了針對IFileManager/FileManager的服務註冊。

class Program
{
    static void Main()
    {
        static void Print(int layer, string name)  => Console.WriteLine($"{new string(' ', layer * 4)}{name}");        
        new ServiceCollection()
            .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test"))
            .AddSingleton<IFileManager, FileManager>()
            .BuildServiceProvider()
            .GetRequiredService<IFileManager>()
            .ShowStructure(Print);
    }
}

我們最終利用ServiceCollection生成的IServiceProvider物件得到FileManager物件,並呼叫該物件的ShowStructure方法將PhysicalFileProvider物件對映的目錄結構呈現出來。當我們執行該程式之後,控制檯上將呈現出如下圖所示的輸出結果,該結果為我們展示了對映物理目錄的真實結構。(S501)

二、讀取檔案內容

前面我們演示瞭如何利用IFileProvider物件將檔案系統的結構完整地呈現出來,接下來我們來演示如何利用它來讀取一個物理檔案的內容。我們為IFileManager定義如下一個ReadAllTextAsync方法以非同步的方式讀取指定檔案內容,方法的引數表示檔案的路徑。如下面的程式碼片段所示,ReadAllTextAsync方法將指定的檔案路徑作為引數呼叫IFileProvider物件的GetFileInfo方法得到一個IFileInfo物件。我們最終呼叫這個IFileInfo物件的CreateReadStream方法得到讀取檔案的輸出流,進而得到檔案的真實內容。

public interface IFileManager
{
    ...
    Task<string> ReadAllTextAsync(string path);
}

public class FileManager : IFileManager
{
    ...
    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, 0, buffer.Length);
        }
        return Encoding.Default.GetString(buffer);
    }
}

假設我們依然將FileManager使用的IFileProvider對映為目錄“c:\test\”,現在我們在該目錄中建立一個名為data.txt的文字檔案,並在該檔案中任意寫入一些內容。接下來我們在Main方法中編寫了如下的程式利用依賴注入的方式得到FileManager物件,並讀取檔案data.txt的內容。最終的除錯斷言旨在確定通過IFileProvider讀取的確實就是目標檔案的真實內容。(S502)

class Program
{
    static async Task Main()
    {
        var content = await new ServiceCollection()
            .AddSingleton<IFileProvider>(new PhysicalFileProvider(@"c:\test"))
            .AddSingleton<IFileManager, FileManager>()
            .BuildServiceProvider()
            .GetRequiredService<IFileManager>()
            .ReadAllTextAsync("data.txt");

        Debug.Assert(content == File.ReadAllText(@"c:\test\data.txt"));
    }
}

三、內嵌檔案系統

我們一直在強調由IFileProvider結構構建的是一個抽象的具有目錄結構的檔案系統,具體檔案的提供方式取決於對具體的IFileProvider物件是怎樣一個型別。我們演示例項定義的FileManager並沒有限定具體使用何種型別的IFileProvider,該物件是在應用中通過依賴注入的方式指定的。由於上面的應用程式注入的是一個PhysicalFileProvider物件,所以我們可以利用它來讀取對應物理目錄下的某個檔案。假設現在將這個data.txt直接以資原始檔的形式編譯到程式集中,我們就需要使用另一個名為EmbeddedFileProvider的實現型別。現在我們直接將這個data.txt檔案新增到控制檯應用的專案根目錄下。在預設的情況下,當我們編譯專案的時候這樣的檔案並不能成為內嵌到目標程式集的資原始檔,我們需要利用VS將該檔案的“Build Action”屬性按照如下所示的方式設定為“Embedded resource”。

上圖所示的設定將會體現在專案檔案(.csproj檔案)上。具體來說,專案檔案會以如下的形式新增一個<EmbeddedResource>元素將檔案data.txt設定為內嵌到編譯後生成的程式集的內嵌資原始檔。

<Project Sdk="Microsoft.NET.Sdk">
  ...
  <ItemGroup>
      <EmbeddedResource Include="data.txt"/>   
  </ItemGroup>
</Project>

我們編寫了如下的程式來演示針對內嵌於程式集中的資原始檔的讀取。我們首先得到當前入口程式集,並利用它建立了一個EmbeddedFileProvider物件,它代替原來的PhysicalFileProvider物件被註冊到ServiceCollection之中。我們接下來採用了完全一致的程式設計方式得到FileManager物件並利用它讀取內嵌檔案data.txt的內容。為了驗證讀取的目標檔案準確無誤,我們採用直接讀取資原始檔的方式得到了內嵌檔案data.txt的內容,並利用一個除錯斷言確定兩者的一致性。(S503)

class Program
{
    static async Task Main()
    {
        var assembly = Assembly.GetEntryAssembly();

        var content1 = await new ServiceCollection()
            .AddSingleton<IFileProvider>(new EmbeddedFileProvider(assembly))
            .AddSingleton<IFileManager, FileManager>()
            .BuildServiceProvider()
            .GetRequiredService<IFileManager>()
            .ReadAllTextAsync("data.txt");

        var stream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.data.txt");
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        var content2 = Encoding.Default.GetString(buffer);

        Debug.Assert(content1 == content2);
    }
}

四、監控檔案的變化

在檔案讀取場景中,確定載入到記憶體中的資料與原始檔的一致性並自動同步是一個很常見的需求。比如說我們將配置定義在一個JSON檔案中,應用啟動的時候會讀取該檔案並將其轉換成對應的Options物件。在很多情況下,如果我們改動了配置檔案, 最新的配置資料只有在應用重啟之後才能生效。如果我們能夠以一種高效的方式對配置檔案進行監控,並在其發生改變的情況下向應用傳送通知,那麼應用就能在不用重啟的情況下重新讀取配置檔案,進而實現Options物件承載的內容和原始配置檔案完全同步。

對檔案系統實施監控並在其發生改變時傳送通知也是IFileProvider物件提供的核心功能之一。接下來我們依然使用前面這個程式來演示如何使用PhysicalFileProvider對某個物理檔案實施監控,並在目標檔案的內容發生改變的時候重新讀取新的內容。

class Program
{
    static async Task Main()
    {
        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, 0, buffer.Length);
                    string current = Encoding.Default.GetString(buffer);
                    if (current != original)
                    {
                        Console.WriteLine(original = current);
                    }
                }
            }
        }
    }
}

如上面的程式碼片段所示,我們針對目錄“c:\test”建立了一個PhysicalFileProvider物件,並呼叫其Watch方法對指定的檔案data.txt實施監控。該方法的返回一個IChangeToken物件,我們正是利用這個物件接收檔案改變的通知。我們呼叫ChangeToken的靜態方法OnChange針對這個物件註冊了一個回撥實現對原始檔的重新讀取和顯示,當原始檔發生改變的時候,註冊的回撥會自動執行。我們以每隔5秒的間隔對檔案data.txt作一次修改,而檔案的內容為當前時間。所以當我們的程式啟動之後,每隔5秒鐘當前時間就會以如下圖的方式呈現在控制檯上。

[ASP.NET Core 3框架揭祕] 檔案系統[1]:抽象的“檔案系統”
[ASP.NET Core 3框架揭祕] 檔案系統[2]:總體設計
[ASP.NET Core 3框架揭祕] 檔案系統[3]:物理檔案系統
[ASP.NET Core 3框架揭祕] 檔案系統[4]:程式集內嵌檔案系統