由EmbeddedFileProvider构建的内嵌(资源)文件系统

一个物理文件可以直接作为资源内嵌到编译生成的程序集中。借助于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对象。

作者:蒋金楠 
微信公众账号:大内老A

由EmbeddedFileProvider构建的内嵌(资源)文件系统的更多相关文章

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

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

  2. ABP官方文档翻译 6.5 内嵌资源文件

    内嵌资源文件 介绍 创建内嵌文件 xproj/project.json形式 csproj形式 添加内嵌资源管理器 使用内嵌视图 使用内嵌资源 ASP.NET Core 配置 忽略文件 重写内嵌文件 介 ...

  3. C#中内嵌资源的读取

    起因 作为一个从Cpper转到C#并且直接从事WPF开发的萌新来说,正式编码过程中碰到了不少问题,一路上磕磕碰碰的.因为软件设计需求上的要求,需要将一些配置文件(XML.INI等)内嵌到程序中,等需要 ...

  4. spring 5.x 系列第20篇 ——spring简单邮件、附件邮件、内嵌资源邮件、模板邮件发送 (代码配置方式)

    源码Gitub地址:https://github.com/heibaiying/spring-samples-for-all 一.说明 1.1 项目结构说明 邮件发送配置类为com.heibaiyin ...

  5. spring 5.x 系列第19篇 ——spring简单邮件、附件邮件、内嵌资源邮件、模板邮件发送 (xml配置方式)

    源码Gitub地址:https://github.com/heibaiying/spring-samples-for-all 一.说明 1.1 项目结构说明 邮件发送配置文件为springApplic ...

  6. [VSTO] 区分MAILITEM的ATTACHMENT是真正的附件还是内嵌资源

    在遍历MailItem的Attachments集合的时候发现,不管是真正的附件还是内嵌资源,比如邮件内容中内嵌的图片(Embedded Image),都是Attachments集合的元素,通过查看at ...

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

    一个物理文件可以直接作为资源内嵌到编译生成的程序集中.借助于EmbeddedFileProvider,我们可以采用统一的编程方式来读取内嵌的资源文件,该类型定义在 "Microsoft.Ex ...

  8. Razor Page Library:开发独立通用RPL(内嵌wwwroot资源文件夹)

    ASP.NET Core知多少系列:总体介绍及目录 Demo路径:GitHub-RPL.Demo 1. Introduction Razor Page Library 是ASP.NET Core 2. ...

  9. golang1.16内嵌静态资源指南

    今天是万圣节,也是golang1.16新特性冻结的日子.不得不说自从go2路线发布之后golang新特性的迭代速度也飞速提升,1.16中有相当多的重要更新,包括io标准库的重构,语言内置的静态资源嵌入 ...

随机推荐

  1. project euler 12 Highly divisible triangular number

    Highly divisible triangular number Problem 12 The sequence of triangle numbers is generated by addin ...

  2. Html 编码 queryUrl = encodeURI(queryUrl);

    Html  编码 queryUrl = encodeURI(queryUrl);

  3. Effective Java从零开始 - 就是爱Java

    或许你已经开始写Java了,或许只是想要一窥这个的世界,无论是抱着何种心情来看Java,从零开始,会一种没有负担,没有包袱的事,你会发现写程序不再是枯燥乏味,孤单寂寞的一个人,而是生活中最快乐的学习之 ...

  4. C# 多线程的自动管理(线程池) 基于Task的方式

    C# 多线程的自动管理(线程池) 在多线程的程序中,经常会出现两种情况:    1. 应用程序中线程把大部分的时间花费在等待状态,等待某个事件发生,然后给予响应.这一般使用 ThreadPool(线程 ...

  5. [ ArcGIS Server技术版]如何得到本机上的所有的REST服务?

    http://server.arcgisonline.com/ArcGIS/rest/services?f=json得到的字符串 {"currentVersion":10.01,& ...

  6. scheme 模拟queue

    [code 1] shows a implementation of queue. The function enqueue! returns a queue in that the obj is a ...

  7. codeforce343A

    题目地址:http://codeforces.com/problemset/problem/343/A 比赛的时候就囧了,只推出a<b的时候最少需要b个电阻. 后来看了题解,知道 题意:用最少的 ...

  8. svn小技巧——重定向svn diff

    svn diff的默认输出模式比较冗长,如果遇到修改比较多的情况,有时会较难看清diff.svn本身提供了自定义diff输出的选项,可能的修改方法如下: 建立一个脚本文件(如svndiff.sh),调 ...

  9. python分布式抓取网页

    呵呵,前两节好像和python没多大关系..这节完全是贴代码, 这是我第一次写python,很多地方比较乱,主要就看看逻辑流程吧. 对于编码格式确实搞得我头大..取下来页面不知道是什么编码,所以先找c ...

  10. Impala 4、Impala JDBC

    • 配置: – impala.driver=org.apache.hive.jdbc.HiveDriver – impala.url=jdbc:hive2://node2:21050/;auth=no ...