.NET Core的文件系统[4]:由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”配置选项,从而将相应的文本文件内嵌到项目编译生成的程序集中。

4

假设我们我们对“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
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
24天前
|
消息中间件 前端开发 小程序
一个基于.NET Core构建的简单、跨平台、模块化的商城系统
今天大姚给大家分享一个基于.NET Core构建的简单、跨平台、模块化、完全开源免费(MIT License)的商城系统:Module Shop。
|
24天前
|
算法 C# 数据库
【干货】一份10万字免费的C#/.NET/.NET Core面试宝典
C#/.NET/.NET Core相关技术常见面试题汇总,不仅仅为了面试而学习,更多的是查漏补缺、扩充知识面和大家共同学习进步。该知识库主要由自己平时学习实践总结、网上优秀文章资料收集(这一部分会标注来源)和社区小伙伴提供三部分组成。该份基础面试宝典完全免费,发布两年来收获了广大.NET小伙伴的好评,我会持续更新和改进,欢迎关注我的公众号【追逐时光者】第一时间获取最新更新的面试题内容。
|
5天前
|
开发框架 缓存 前端开发
利用Visual Basic构建高效的ASP.NET Web应用
【4月更文挑战第27天】本文探讨使用Visual Basic与ASP.NET创建高效Web应用的策略,包括了解两者基础、项目规划、MVC架构、数据访问与缓存、代码优化、异步编程、安全性、测试及部署维护。通过这些步骤,开发者能构建出快速、可靠且安全的Web应用,适应不断进步的技术环境。
|
3天前
|
机器学习/深度学习 自然语言处理 安全
【专栏】.NET 开发:构建智能应用的关键
【4月更文挑战第29天】本文探讨了.NET开发在构建智能应用中的关键作用,强调了其强大的框架、工具集、高效性能和跨平台支持。通过实例展示了.NET在人工智能、物联网及企业级应用中的应用。同时,指出了.NET开发面临的挑战,如技术更新的学习成本、性能优化、资源管理和安全隐私保护,并提出了应对策略。随着技术进步,.NET将在智能应用领域发挥更大作用,推动创新与便利。
|
4天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
17 1
|
7天前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
17 0
|
22天前
|
Linux API iOS开发
.net core 优势
.NET Core 的优势:跨平台兼容(Windows, macOS, Linux)及容器支持,高性能,支持并行版本控制,丰富的新增API,以及开源。
25 4
|
4月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
45 0
|
2月前
|
开发框架 前端开发 .NET
进入ASP .net mvc的世界
进入ASP .net mvc的世界
32 0
|
2月前
mvc.net分页查询案例——mvc-paper.css
mvc.net分页查询案例——mvc-paper.css
5 0