一個物理文件可以直接作為資源內嵌到編譯生成的程序集中。借助于EmbeddedFileProvider,我們可以統一的編程方式來讀取內嵌于某個程序集中的資源文件,不過在這之前我們必須知道如何將一個項目文件作為資源并嵌入到生成的程序集中。 [ 本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、將項目文件變成內嵌資源
二、讀取資源文件
三、EmbededFileProvider
一、將項目文件變成內嵌資源
在默認情況下,我們添加到一個.NET項目中的靜態文件并不會成為項目編譯生成的程序集的內嵌資源文件。如果需要,我們需要通過修改project.json文件中與編譯相關的設置顯式地將某個項目文件添加到內嵌資源文件列表中,這個與內嵌資源相關的配置選項就是“buildOptions/embed”。“buildOptions/embed”的配置結構比較典型,project.json文件中涉及到文件選擇策略的絕大部分配置選項幾乎都采用了這樣的結構。除了用于選在內嵌資源文件的配置選項“buildOptions/embed”,其他與文件選擇相關的配置選項還如下這些:
- buildOptions/compile:從當前項目中選擇參與編譯的源文件。
- buildOptions/copyToOutput:從當前項目中選擇在編譯時自動拷貝到輸出目錄(默認為bin目錄)的文件。
- packOptions/files:從當前項目中選擇在打包的時候添加到生車的NuGet包的文件。
- publishOptions:從當前項目中選擇需要發布的文件。
對于包括“buildOptions/embed”在內的上述這五種配置選項,我們可以指定一個對象作為它的值。這個配置對象如下表所示的6個屬性,我們可以利用“include”和“execlude”屬性以Globbing Pattern表達式指定“包含”和“排除”的一組文件,也可以利用“includeFiles”和“execludeFiles”屬性以文件路徑(不含通配符)的形式將具體指定的文件“包含進來”或者“排除出去”。這些配置從本質上體現了針對一組項目文件的“轉移”,在默認的情況源文件和目標文件具有完全一致的名稱和相對路徑,如果目標文件的路徑或者名稱不同,我們可以利用mapping屬性對兩者做一個映射。這些屬性體現的路徑都將項目所在的目錄作為根路徑。
屬性 | 數據類型 | 描述 |
include | string/string[] | 以Globbing Pattern表達式形式指定的需要被包含進來的文件。 |
execlude | string/string[] | 以Globbing Pattern表達式形式指定的需要被排除出去的文件。它比include屬性具有更高的優先級,所以如果include和exclude涉及到同一個文件,該文件會被排除出去。 |
includeFiles | string/string[] | 以文件路徑形式指定的需要被包含進來的文件。它比exclude屬性具有更高的優先級,所以execlude將某個文件排除出去,我們可以利用includeFiles屬性將它重新包含進來。 |
execludeFiles | string/string[] | 以文件路徑形式指定的需要被包含進來的文件。它的優先級比上述三個屬性都高,所以include將某個文件包含進來后,我們可以利用excludeFiles屬性將它重新排除出去。 |
buildIns | object | 這個對象具有include和exclude兩個屬性,表示系統默認提供的文件。builtIns的include和execlude屬性與上述的同名屬性具有相同的定義方式和作用。如果我們對include和builtIns/include(或者execlude和builtIns/execlude)都做了配置,系統在計算最終選擇的文件列表時會對它們進行合并。 |
mappings | map | 轉移過程源文件和目標文件在路徑布局上的映射關系,其中Key代表目標文件的路徑,至于Value,我們可以設置為源文件的路徑,也可以設置為包含include, exclude,includeFiles and excludeFiles屬性的對象。 |
接下來我們通過簡單的實例來演示如何在project.json文件中對“buildOptions/embed”配置選項進行合理的設置從而將我們希望的文件內嵌到編譯生成的程序集中。我們創建了一個空的.NET Core項目,并按照如下圖所示的結構在根目錄下創建了一個名為“root”的目錄。總的來說該目錄(含其子目錄)一共包含4個文本文件,我們現在需要通過在project.json文件中設置它的“buildOptions/embed”配置選項,從而將相應的文本文件內嵌到項目編譯生成的程序集中。
假設我們我們對“buildOptions/embed”配置選項做了如下三種不同的設置。由于include|exclude與builtIns/include|builtIns/exclude具有相同的作用,所以前三種定義方式在文件選擇的角度上講是完全等效的,最終作為內嵌資源的文件只有兩個,那就是“root/dir1/foobar/foo.txt” 和“root/dir1/baz.txt”。在默認的情況下,內嵌的資源文件是根據源文件在項目中的路徑來命名的,具體的命名規則為“{程序集名稱}.{文件路徑}”(路徑分隔符替換成“.”),所以這兩個資源文件的名稱為“App.root.dir1.foobar.foo.txt”與“App.root.dir1.baz.txt”。對于第三種定義方式,我們通過mappings屬性做了一個簡單的路徑映射,進而將兩個資源文件的名稱改成“foo.txt”和“baz.txt”。
定義1
1: {
2: ...
3: "buildOptions": {
4: ...
5: "embed": {
6: "include" : "root/**/*.txt",
7: "exclude" : "root/dir1/foobar/*.txt",
8: "includeFiles" : "root/dir1/foobar/foo.txt",
9: "excludeFiles" : "root/dir2/gux.txt"
10: }
11: }
12: }
定義2
1: {
2: ...
3: "buildOptions": {
4: ...
5: "embed": {
6: "builtIns": {
7: "include": "root/**/*.txt",
8: "exclude": "root/dir1/foobar/*.txt"
9: },
10: "includeFiles" : "root/dir1/foobar/foo.txt",
11: "excludeFiles" : "root/dir2/gux.txt"
12: }
13: }
14: }
定義3
1: {
2: ...
3: "buildOptions": {
4: ...
5: "embed": {
6: "builtIns": {
7: "include": "root/**/*.txt",
8: "exclude": "root/dir1/foobar/*.txt"
9: },
10: "includeFiles" : "root/dir1/foobar/foo.txt",
11: "excludeFiles" : "root/dir2/gux.txt"
12:
13: "mappings": {
14: "foo.txt": "root/dir1/foobar/foo.txt",
15: "baz.txt": "root/dir1/baz.txt"
16: }
17: }
18: }
19: }
除了將“buildOptions/embed”配置選項設置為上述這么一個對象之外,我們還具有一個更加簡單的設置方式,那就是直接設置為一個Globbing Pattern表達式或者表達式數組。這樣的設置相當于是將設置的Globbing Pattern表達式添加到incude列表中,所以如下所示的兩種配置是完全等效的。
定義1
1: {
2: ...
3: "buildOptions": {
4: ...
5: "embed": {
6: "include" : ["root/**/foo.txt","root/**/bar.txt"]
7: }
8: }
9: }
定義2
1: {
2: ...
3: "buildOptions": {
4: ...
5: "embed" : ["root/**/foo.txt","root/**/bar.txt"]
6: }
7: }
8: }
二、讀取資源文件
每個程序集都有一個清單文件(Manifest),它的一個重要作用就是記錄組成程序集的所有文件。總的來說,一個程序集主要由兩種類型的文件構成,它們分別是承載IL代碼的托管模塊文件和編譯時內嵌的資源文件。針對圖4所示的項目結果,如果我們將四個文本文件以資源文件的形式內嵌到生成的程序集(App.dll)中,程序集的清單文件將會采用如下所示的形式來記錄它們。
1: .mresource public App.root.dir1.baz.txt
2: {
3: // Offset: 0x00000000 Length: 0x0000000C
4: }
5: .mresource public App.root.dir1.foobar.bar.txt
6: {
7: // Offset: 0x00000010 Length: 0x0000000C
8: }
9: .mresource public App.root.dir1.foobar.foo.txt
10: {
11: // Offset: 0x00000020 Length: 0x0000000C
12: }
13: .mresource public App.root.dir2.gux.txt
14: {
15: // Offset: 0x00000030 Length: 0x0000000C
16: }
表示程序集的Assembly對象定義了如下幾個方法來提取內嵌資源的文件的相關信息和讀取指定資源文件的內容。GetManifestResourceNames方法幫助我們獲取記錄在程序集清單文件中的資源文件名,而另一個方法GetManifestResourceInfo則獲取指定資源文件的描述信息。如果我們需要讀取某個資源文件的內容,我們可以將資源文件名稱作為參數調用GetManifestResourceStream方法,該方法會返回一個讀取文件內容的輸出流。
1: public abstract class Assembly
2: {
3: public virtual string[] GetManifestResourceNames();
4: public virtual ManifestResourceInfo GetManifestResourceInfo(string resourceName);
5: public virtual Stream GetManifestResourceStream(string name);
6: }
三、EmbededFileProvider
在對內嵌于程序集的資源文件有了大致的了解之后,針對與對應的EmbeddedFileProvider的實現原理就很好理解了。雖然編譯之前的原始文件以目錄的形式進行組織,但是當我們內嵌到程序集之后,目錄結構將不復存在,我們可以理解為所有的資源文件都保存在程序集的“根目錄”下。所以在通過 EmbeddedFileProvider構建的文件系統中并沒有目錄層級的概念,它的FileInfo對象總是對一個具體資源文件的描述。具體來說,這個藐視資源文件的FileInfo是如下一個名為EmbeddedResourceFileInfo對象,EmbeddedResourceFileInfo類型定義在NuGet包“Microsoft.Extensions.FileProviders.Embedded”之中。
1: public class EmbeddedResourceFileInfo : IFileInfo
2: {
3: private readonly Assembly _assembly;
4: private long? _length;
5: private readonly string _resourcePath;
6:
7: public EmbeddedResourceFileInfo(Assembly assembly, string resourcePath, string name, DateTimeOffset lastModified)
8: {
9: _assembly = assembly;
10: _resourcePath = resourcePath;
11: this.Name = name;
12: this.LastModified = lastModified;
13: }
14:
15: public Stream CreateReadStream()
16: {
17: Stream stream = _assembly.GetManifestResourceStream(_resourcePath);
18: if (!this._length.HasValue)
19: {
20: this._length = new long?(stream.Length);
21: }
22: return stream;
23: }
24:
25: public bool Exists
26: {
27: get { return true; }
28: }
29:
30: public bool IsDirectory
31: {
32: get { return false; }
33: }
34:
35: public DateTimeOffset LastModified { get; private set; }
36:
37: public long Length
38: {
39: get
40: {
41: if (!this._length.HasValue)
42: {
43: using (Stream stream =_assembly.GetManifestResourceStream(this._resourcePath))
44: {
45: _length = new long?(stream.Length);
46: }
47: }
48: Return _length.Value;
49: }
50: }
51:
52: public string Name { get; private set;}
53:
54: public string PhysicalPath
55: {
56: get { return null; }
57: }
58: }
如上面的代碼片段所示,我們在創建一個EmbeddedResourceFileInfo對象的時候需要指定內嵌資源文件在清單文件的中的名稱(resourcePath)和所在的程序集,以及資源文件的“邏輯”名稱(name)。由于一個EmbeddedResourceFileInfo對象總是對應著一個具體的內嵌資源文件,所以它的Exists屬性返回True,IsDirectory屬性返回False。由于資源文件系統并不具有層次還的目錄結構,它所謂的物理路徑毫無意義,所以PhysicalPath屬性直接返回Null。CreateReadStream方法返回的是調用程序集的GetManifestResourceStream方法返回的輸出流,而表示文件長度的Length返回的是這個Stream對象的長度。
如下所示的是 EmbeddedFileProvider的定義。當我們在創建一個EmbeddedFileProvider對象的時候,除了指定資源文件所在的程序集之外,還可以指定一個命名空間。對于由EmbeddedFileProvider構建的內嵌資源文件系統來說,文件的名稱和這個命名空間共同組成資源文件在程序集清單中的文件名。同樣以上圖所示的這個項目為例,資源文件foo.txt在程序集清單中的文件名稱為“App.root.dir1.foobar.foo.txt”,如果EmbeddedFileProvider采用的“App.root”作為命名空間,那么對應的資源文件在邏輯上的名稱就應該是“dir1.foobar.foo.txt”,這就是我們在上面所謂的資源文件的邏輯名稱。如果該命名空間沒作顯式設置,默認情況下會將程序集的名稱“App”作為命名空間,那么這個資源文件的名稱就應該是“root.dir1.foobar.foo.txt”。
1: public class EmbeddedFileProvider : IFileProvider
2: {
3: public EmbeddedFileProvider(Assembly assembly);
4: public EmbeddedFileProvider(Assembly assembly, string baseNamespace);
5:
6: public IDirectoryContents GetDirectoryContents(string subpath);
7: public IFileInfo GetFileInfo(string subpath);
8: public IChangeToken Watch(string pattern);
9: }
當我們指定資源文件的邏輯名稱調用EmbeddedFileProvider的GetFileInfo方法時,該方法會將它與命名空間一起組成資源文件在程序集清單的名稱(路徑分隔符會被替換成“.”)。如果對應的資源文件存在,那么一個EmbeddedResourceFileInfo會被創建并返回,否則返回的將是一個NotFoundFileInfo對象。對于內嵌資源文件系統來說,根本就不存在所謂的文件更新的問題,所以它的Watch方法會返回一個HasChanged永遠返回False的ChangeTokne對象。
由于 EmbeddedFileProvider構建的內嵌資源文件系統不存在層次化的目錄結構,所有的資源文件可以視為統統存儲在程序集的“根目錄”下,所以它的GetDirectoryContents方法只有在我們指定一個空字符串或者“/”(空字符串和“/”都表示“根目錄”)時才會返回一個描述這個“根目錄”的DirectoryContents對象,該對象實際上是一組EmbeddedResourceFileInfo對象的集合。在其他情況下,EmbeddedFileProvider的GetDirectoryContents方法總是返回一個NotFoundDirectoryContents對象。
文章列表