[ASP.NET Core 3框架揭秘] 文件系統[4]:程序集內嵌文件系統

一個物理文件可以直接作為資源內嵌到編譯生成的程序集中。藉助於EmbeddedFileProvider,我們可以採用統一的編程方式來讀取內嵌的資源文件,該類型定義在 “Microsoft.Extensions.FileProviders.Embedded”這個NuGet包中。在正式介紹EmbeddedFileProvider之前,我們必須知道如何將一個項目文件作為資源內嵌入到編譯生成的程序集中。

一、將項目文件變成內嵌資源

在默認情況下,我們添加到一個.NET Core項目中的靜態文件並不會成為目標程序集的內嵌資源文件。如果需要將靜態文件作為目標程序集的內嵌文件,我們需要修改當前項目對應的.csproj文件。具體來說,我們需要按照前面實例演示的方式在.csproj文件中添加<ItemGroup>/<EmbeddedResource>元素,並利用Include屬性顯式地將對應的資源文件包含進來。當我們直接利用Visual Studio將資源文件的Build Action屬性設置為“Embedded resource”,IDE會自動幫助我們修改項目文件。

<EmbeddedResource>的Include屬性可以設置多個路徑,路徑之間採用分號(“;”)作為分隔符。以上圖所示的目錄結構為例,如果我們需要將root目錄下的四個文件作為程序集的內嵌文件,我們可以修改.csproj文件並按照如下的形式將四個文件的路徑包含進來。

<Project Sdk="Microsoft.NET.Sdk">
    ...
    <ItemGroup>
        <EmbeddedResource  
            Include="root/dir1/foobar/foo.txt;root/dir1/foobar/bar.txt;root/dir1/baz.txt;root/dir2/qux.txt"></EmbeddedResource> 
    </ItemGroup>
</Project>

除了指定每個需要內嵌的資源文件的路徑之外,我們還可以採用基於通配符“*”和“**”的Globbing Pattern表達式將一組匹配的文件批量包含進來。同樣是將root目錄下的所有文件作為程序集的內嵌文件,如下的定義方式就會簡潔得多。

<Project Sdk="Microsoft.NET.Sdk">
    ...
    <ItemGroup>
        <EmbeddedResource  Include="root/**"></EmbeddedResource> 
    </ItemGroup>
</Project>

<EmbeddedResource>除了具有一個Include屬性用來添加內嵌資源文件之外,它還具有另一個Exclude屬性負責將不符合要求的文件排除出去。還是以前面這個項目為例,對於root目錄下的四個文件,如果我們不希望文件baz.txt作為內嵌資源文件,我們可以按照如下的方式將它排除。

<Project Sdk="Microsoft.NET.Sdk">
    ...
    <ItemGroup>
        <EmbeddedResource  Include="root/**" Exclude="root/dir1/baz.txt"></EmbeddedResource> 
    </ItemGroup>
</Project>

二、讀取資源文件

每個程序集都有一個清單文件(Manifest),它的一個重要作用就是記錄組成程序集的所有文件成員。總的來說,一個程序集主要由兩種類型的文件構成,它們分別是承載IL代碼的託管模塊文件和編譯時內嵌的資源文件。針對上圖所示的項目結構,如果我們將四個文本文件以資源文件的形式內嵌到生成的程序集(App.dll)中,程序集的清單文件將會採用如下所示的形式來記錄它們。

.mresource public App.root.dir1.baz.txt
{
  // Offset: 0x00000000 Length: 0x0000000C
}
.mresource public App.root.dir1.foobar.bar.txt
{
  // Offset: 0x00000010 Length: 0x0000000C
}
.mresource public App.root.dir1.foobar.foo.txt
{
  // Offset: 0x00000020 Length: 0x0000000C
}
.mresource public App.root.dir2.qgux.txt
{
  // Offset: 0x00000030 Length: 0x0000000C
}

雖然文件在原始的項目中具有層次化的目錄結構,但是當它們成功轉移到編譯生成的程序集中之後,目錄結構將不復存在,所有的內嵌文件將統一存放在同一個容器中。如果我們通過Reflector打開程序集,資源文件的扁平化存儲將會一目瞭然。為了避免命名衝突,編譯器將會根據原始文件所在的路徑來對資源文件重新命名,具體的規則是“{BaseNamespace}.{Path}”,目錄分隔符將統一轉換成“.”。值得強調的是資源文件名稱的前綴不是程序集的名稱,而是我們為項目設置的基礎命名空間的名稱。

表示程序集的Assembly對象定義了如下幾個方法來提取內嵌資源的文件的相關信息和讀取指定資源文件的內容。GetManifestResourceNames方法幫助我們獲取記錄在程序集清單文件中的資源文件名,而另一個方法GetManifestResourceInfo則用於獲取指定資源文件的描述信息。如果我們需要讀取某個資源文件的內容,我們可以將資源文件名稱作為參數調用GetManifestResourceStream方法,該方法會返回一個讀取文件內容的Stream對象。

public abstract class Assembly
{   
    public virtual string[] GetManifestResourceNames();
    public virtual ManifestResourceInfo GetManifestResourceInfo(string resourceName);
    public virtual Stream GetManifestResourceStream(string name);
}

同樣是針對前面這個演示項目對應的目錄結構,當四個文件作為內嵌文件被成功轉移到編譯生成的程序集中后,我們可以調用程序集對象的GetManifestResourceNames方法獲取這四個內嵌文件的資源名稱。如果以資源名稱(“App.root.dir1.foobar.foo.txt”)作為參數調用GetManifestResourceStream方法,我們可以讀取資源文件的內容,具體的演示如下所示。

class Program
{
    static void Main()
    {
        var assembly = typeof(Program).Assembly;
        var resourceNames = assembly.GetManifestResourceNames();
        Debug.Assert(resourceNames.Contains("App.root.dir1.foobar.foo.txt"));
        Debug.Assert(resourceNames.Contains("App.root.dir1.foobar.bar.txt"));
        Debug.Assert(resourceNames.Contains("App.root.dir1.baz.txt"));
        Debug.Assert(resourceNames.Contains("App.root.dir2.qgux.txt")); 

        var stream = assembly.GetManifestResourceStream("App.root.dir1.foobar.foo.txt");
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        var content = Encoding.Default.GetString(buffer);  
        Debug.Assert(content == File.ReadAllText("App/root/dir1/foobar/foo.txt"));
    }
}

三、EmbeddedFileProvider

在對內嵌於程序集的資源文件有了大致的了解之後,針對EmbeddedFileProvider的實現原理就很好理解了。由於內嵌於程序集的資源文件採用扁平化存儲形式,所以在通過 EmbeddedFileProvider構建的文件系統中並沒有目錄層級的概念。我們可以認為所有的資源文件都保存在程序集的“根目錄”下。對於EmbeddedFileProvider構建的文件系統來說,它提供的IFileInfo對象總是對一個具體資源文件的描述,這是一個具有如下定義的EmbeddedResourceFileInfo對象。

public class EmbeddedResourceFileInfo : IFileInfo
{
    private readonly Assembly     _assembly;
    private long? _length;
    private readonly string  _resourcePath;

    public EmbeddedResourceFileInfo(Assembly assembly, string resourcePath, string name, DateTimeOffset lastModified)
    {
        _assembly = assembly;
        _resourcePath = resourcePath;
        this.Name = name;
        this.LastModified = lastModified;
    }

    public Stream CreateReadStream()
    {
        Stream stream = _assembly.GetManifestResourceStream(_resourcePath);
        if (!this._length.HasValue)
        {
            this._length = new long?(stream.Length);
        }
        return stream;
    }
    
    public bool Exists => true;
    public bool IsDirectory => false;
    public DateTimeOffset LastModified { get; }    

    public string Name { get; }
    public string PhysicalPath => null;
    public long Length
    {
        get
        {
            if (!_length.HasValue)
            {
                using (Stream stream =_assembly.GetManifestResourceStream(this._resourcePath))
                {
                    _length = stream.Length;
                }
            }
            rReturn _length.Value;
        }
    }
}

如上面的代碼片段所示,我們在創建一個EmbeddedResourceFileInfo對象的時候需要指定內嵌資源文件在清單文件的中的路徑(resourcePath)、所在的程序集、資源文件的名稱(name)和作為文件最後修改時間的DateTimeOffset對象。由於一個EmbeddedResourceFileInfo對象總是對應着一個具體的內嵌資源文件,所以它的Exists屬性總是返回True,IsDirectory屬性則返回False。由於資源文件系統並不具有層次化的目錄結構,它所謂的物理路徑毫無意義,所以PhysicalPath屬性直接返回Null。CreateReadStream方法返回的是調用程序集的GetManifestResourceStream方法返回的輸出流,而表示文件長度的Length返回的是這個Stream對象的長度。

如下所示的是 EmbeddedFileProvider的定義。當我們在創建一個EmbeddedFileProvider對象的時候,除了指定資源文件所在的程序集之外,還可以指定一個基礎命名空間。如果該命名空間沒作顯式設置,默認情況下會將程序集的名稱作為命名空間,也就是說如果我們為項目指定了一個不同於程序集名稱的基礎命名空間,那麼當創建這個EmbeddedFileProvider對象的時候必須指定這個命名空間。

public class EmbeddedFileProvider : IFileProvider
{   
    public EmbeddedFileProvider(Assembly assembly);
    public EmbeddedFileProvider(Assembly assembly, string baseNamespace);

    public IDirectoryContents GetDirectoryContents(string subpath);
    public IFileInfo GetFileInfo(string subpath);
    public IChangeToken Watch(string pattern);
}

當我們調用EmbeddedFileProvider的GetFileInfo方法並指定資源文件的邏輯名稱時,該方法會將它與命名空間一起組成資源文件在程序集清單的名稱(路徑分隔符會被替換成“.”)。如果對應的資源文件存在,那麼一個EmbeddedResourceFileInfo會被創建並返回,否則返回的將是一個NotFoundFileInfo對象。對於內嵌資源文件系統來說,根本就不存在所謂的文件更新的問題,所以它的Watch方法會返回一個HasChanged屬性總是False的IChangeToken對象。

由於內嵌於程序集的資源文件總是只讀的,它所謂的最後修改時間實際上是程序集的生成日期,所以EmbeddedFileProvider在提供EmbeddedResourceFileInfo對象的時候會採用程序集文件的最後更新時間作為資源文件的最後更新時間。如果不能正確地解析出這個時間,EmbeddedResourceFileInfo的LastModified屬性將被設置為當前UTC時間。

由於 EmbeddedFileProvider構建的內嵌資源文件系統不存在層次化的目錄結構,所有的資源文件可以視為統統存儲在程序集的“根目錄”下,所以它的GetDirectoryContents方法只有在我們指定一個空字符串或者“/”(空字符串和“/”都表示“根目錄”)時才會返回一個描述這個“根目錄”的DirectoryContents對象,該對象實際上是一組EmbeddedResourceFileInfo對象的集合。在其他情況下,EmbeddedFileProvider的GetDirectoryContents方法總是返回一個NotFoundDirectoryContents對象。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享