[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对象。
[ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”
[ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计
[ASP.NET Core 3框架揭秘] 文件系统[3]:物理文件系统
[ASP.NET Core 3框架揭秘] 文件系统[4]:程序集内嵌文件系统
[ASP.NET Core 3框架揭秘] 文件系统[4]:程序集内嵌文件系统的更多相关文章
- 《ASP.NET Core 3框架揭秘》读者群,欢迎加入
作为一个17年的.NET开发者,我对一件事特别不能理解:我们的计算机图书市场充斥着一系列介绍ASP.NET Web Forms.ASP.NET MVC.ASP.NET Web API的书籍,但是却找不 ...
- [ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”
ASP.NET Core应用 具有很多读取文件的场景,比如配置文件.静态Web资源文件(比如CSS.JavaScript和图片文件等)以及MVC应用的View文件,甚至是直接编译到程序集中的内嵌资源文 ...
- [ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计
在<抽象的"文件系统">中,我们通过几个简单的实例演示从编程的角度对文件系统做了初步的体验,接下来我们继续从设计的角度来进一步认识它.这个抽象的文件系统以目录的形式来组 ...
- [ASP.NET Core 3框架揭秘] 文件系统[3]:物理文件系统
ASP.NET Core应用中使用得最多的还是具体的物理文件,比如配置文件.View文件以及作为Web资源的静态文件.物理文件系统由定义在NuGet包"Microsoft.Extension ...
- ASP.NET Core 6框架揭秘实例演示[07]:文件系统
ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...
- [ASP.NET Core 3框架揭秘] 依赖注入:控制反转
ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入.文件系统.配置选项和诊断日志等.这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样 ...
- [ASP.NET Core 3框架揭秘] 配置[7]:多样化的配置源[中篇]
物理文件是我们最常用到的原始配置载体,而最佳的配置文件格式主要有三种,它们分别是JSON.XML和INI,对应的配置源类型分别是JsonConfigurationSource.XmlConfigura ...
- [ASP.NET Core 3框架揭秘] 跨平台开发体验: Linux
如果想体验Linux环境下开发.NET Core应用,我们有多种选择.一种就是在一台物理机上安装原生的Linux,我们可以根据自身的喜好选择某种Linux Distribution,目前来说像RHEL ...
- 《ASP.NET Core 3框架揭秘》博文汇总
在过去一段时间内,写了一系列关于ASP.NET Core 3相关的文章,其中绝大部分来源于即将出版的<ASP.NET Core 3框架揭秘>(博文只能算是"初稿",与书 ...
随机推荐
- 一篇文章搞定Python多进程(全)
1.Python多进程模块 Python中的多进程是通过multiprocessing包来实现的,和多线程的threading.Thread差不多,它可以利用multiprocessing.Proce ...
- 五分钟了解物联网SIM卡 | 我的物联网成长记10
[摘要] SIM卡是移动通信中不可或缺的组成部分,在物联网解决方案中,设备移动上网也需要使用SIM卡.那么,SIM卡是什么?SIM卡有几种?各种SIM卡有什么区别?本文将为您答疑解惑. 通信进化史 过 ...
- 在React项目中添加ESLint
1. 安装eslint npm install eslint --save-dev // 或者 yarn add eslint --dev 2. 初始化配置文件 npx eslint --init / ...
- 【数据结构】之栈(C语言描述)
栈(Stack)是编程中最常用的数据结构之一. 栈的特点是“后进先出”,就像堆积木一样,堆的时候要一块一块堆到最上面,拆的时候需要从最上面一块一块往下拆.栈的原理也一样,只不过它的操作不叫堆和拆,而是 ...
- python-布隆过滤器
在学习redis过程中提到一个缓存击穿的问题, 书中参考的解决方案之一是使用布隆过滤器, 那么就有必要来了解一下什么是布隆过滤器.在参考了许多博客之后, 写个总结记录一下. 一.布隆过滤器简介 什么是 ...
- ThreadLocal 源码解读
一.引入 public class Thread implements Runnable { /* 前面略 */ /* ThreadLocal values pertaining to this th ...
- 企业DevOps研发模式下CI/CD实践详解指南
阅读全文大概需要 10分钟. 1. 前言 借着公司今年新组建的中台研发部东风,我作为其中的主要负责人,在研发中心主导推行DevOps研发管理模式转变及质量管理创新建设,本篇文章摘取自今年9月底,笔者在 ...
- XCode项目配置
此设置优先级在playersetting之上,如果为空或者格式不正确或者文件不存在将不会设置,请注意 一.设置面板 二.对应Xcode中设置 1.TeamID 登录苹果开发者网站,查看个人信息,就有 ...
- 【全栈修炼】OAuth2 修炼宝典
一.OAuth 概念 开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用. -- 维基百 ...
- nginx之 nginx限流配置
limit_req zone=req_zone;严格依照在limti_req_zone中配置的rate来处理请求超过rate处理能力范围的,直接drop表现为对收到的请求无延时limit_req zo ...