.NET Core的文件系统[4]:由EmbeddedFileProvider构建的内嵌(资源)文件系统

一个物理文件可以直接作为资源内嵌到编译生成的程序集中。借助于EmbeddedFileProvider,我们可以统一的编程方式来读取内嵌于某个程序集中的资源文件,不过在这之前我们必须知道如何将一个项目文件作为资源并嵌入到生成的程序集中。

目录 一、将项目文件变成内嵌资源 二、读取资源文件 三、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属性对两者做一个映射。这些属性体现的路径都将项目所在的目录作为根路径。

image.png

下来我们通过简单的实例来演示如何在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对象。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java达人

DWR简介

image.png DWR(Direct Web Remoting)是一个WEB远程调用框架.利用这个框架可以让AJAX开发变得很简单.利用DWR可以在客户...

20210
来自专栏阮一峰的网络日志

require() 源码解读

2009年,Node.js 项目诞生,所有模块一律为 CommonJS 格式。 时至今日,Node.js 的模块仓库 npmjs.com ,已经存放了15万个模...

3668
来自专栏自由而无用的灵魂的碎碎念

解决source insight 3.5遇到的parse too comples错误

我使用source insight 编写c,编写代码的时候,source insight的symbol windows可能会提示parse too comple...

1222
来自专栏深度学习之tensorflow实战篇

python(Django之html模板继承)

Django之html模板继承简单案例 1 构建母板,确定不变内容和可变内容 ? 2 构建子板,对可变内容进行填充 ? 结果: ? Dja...

3995
来自专栏web编程技术分享

【Java框架型项目从入门到装逼】第五节 - 在Servlet中接收和返回数据

3447
来自专栏数据之美

浅谈 java 中构建可执行 jar 包的几种方式

        有时候,由于项目的需要,我们会将源码编译后以工具包(class打成jar包)的形式对外提供,此时, 你的 jar 包不一定要是可执行的,只...

4405
来自专栏Java学习123

Python操作文件目录

3526
来自专栏崔庆才的专栏

利用 Flask+Redis 维护 IP 代理池

目前有很多网站提供免费代理,而且种类齐全,比如各个地区、各个匿名级别的都有,不过质量实在不敢恭维,毕竟都是免费公开的,可能一个代理无数个人在用也说不定。所以我们...

2.2K0
来自专栏逍遥剑客的游戏开发

Tiled源码分析(四): 插件机制

2337
来自专栏代码GG之家

深入Android源码系列(二) HOOK技术大作战

漫天的标题党的口水文打赏爆表,冷落了一群默默输出高质量文章的人群。真正的技术文章能否得到认可? 本文讲解内容有 hook技术原理探究 ...

2665

扫码关注云+社区

领取腾讯云代金券