[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框架揭秘>(博文只能算是"初稿",与书 ...
随机推荐
- SpringCloud Alibaba微服务实战一 - 基础环境准备
Springcloud Aibaba现在这么火,我一直想写个基于Springcloud Alibaba一步一步构建微服务架构的系列博客,终于下定决心从今天开始本系列文章的第一篇 - 基础环境准备. 该 ...
- block的本质
全局变量
- Jpa支持LocalDateTime类型持久化
package com.boldseas.porscheshop.common.config; import javax.persistence.AttributeConverter; import ...
- 工作中的开发过程(Javaweb路线,写给刚刚实习或者马上就要工作的朋友)
工作中的开发过程(Javaweb路线,写给刚刚实习或者马上就要工作的朋友) 当我还没开始工作的时候,我是对实际项目开发流程充满未知和向往的,当时很希望能够有一个过来人,给我介绍一下实际工作起来是什么样 ...
- Nginx 404 Not Found 解决办法
环境:宝塔Nginx面板 解决办法: 宝塔面板--站点设置-配置文件. 去掉: error_page 404 /404.html; 前面的 # 号.
- C 可变参数函数的本质
C语言支持定义可变参数的函数,方法是在函数的参数列表最后加上 " ... ",代表变长的参数列表,例如: void Func(int num, ...) { } 需要注意 “... ...
- springboot读取resource下的文件
public static String DEFAULT_CFGFILE = ConfigManager.class.getClassLoader().getResource("conf/s ...
- ARTS-S mac终端ftp命令行上传下载文件
上传 ftp -u ftp://root:123456@10.11.12.3/a.txt a.txt 下载 ftp -o a.txt ftp://root:123456@10.11.12.13/a.t ...
- 谈一谈AOP面向切面编程
AOP是什么 : AOP面向切面编程他是一种编程思想,是指在程序运行期间,将某段代码动态的切入到指定方法的指定位置,将这种编程方式称为面向切面编程 AOP使用场景 : 日志 事务 使用AOP的好处是: ...
- 轻松构建基于 Serverless 架构的弹性高可用音视频处理系统
前言 随着计算机技术和 Internet 的日新月异,视频点播技术因其良好的人机交互性和流媒体传输技术倍受教育.娱乐等行业青睐,而在当前, 云计算平台厂商的产品线不断成熟完善, 如果想要搭建视频点播类 ...