一个物理文件可以直接作为资源内嵌到编译生成的程序集中。借助于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]:程序集内嵌文件系统的更多相关文章

  1. 《ASP.NET Core 3框架揭秘》读者群,欢迎加入

    作为一个17年的.NET开发者,我对一件事特别不能理解:我们的计算机图书市场充斥着一系列介绍ASP.NET Web Forms.ASP.NET MVC.ASP.NET Web API的书籍,但是却找不 ...

  2. [ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”

    ASP.NET Core应用 具有很多读取文件的场景,比如配置文件.静态Web资源文件(比如CSS.JavaScript和图片文件等)以及MVC应用的View文件,甚至是直接编译到程序集中的内嵌资源文 ...

  3. [ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计

    在<抽象的"文件系统">中,我们通过几个简单的实例演示从编程的角度对文件系统做了初步的体验,接下来我们继续从设计的角度来进一步认识它.这个抽象的文件系统以目录的形式来组 ...

  4. [ASP.NET Core 3框架揭秘] 文件系统[3]:物理文件系统

    ASP.NET Core应用中使用得最多的还是具体的物理文件,比如配置文件.View文件以及作为Web资源的静态文件.物理文件系统由定义在NuGet包"Microsoft.Extension ...

  5. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  6. [ASP.NET Core 3框架揭秘] 依赖注入:控制反转

    ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入.文件系统.配置选项和诊断日志等.这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样 ...

  7. [ASP.NET Core 3框架揭秘] 配置[7]:多样化的配置源[中篇]

    物理文件是我们最常用到的原始配置载体,而最佳的配置文件格式主要有三种,它们分别是JSON.XML和INI,对应的配置源类型分别是JsonConfigurationSource.XmlConfigura ...

  8. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Linux

    如果想体验Linux环境下开发.NET Core应用,我们有多种选择.一种就是在一台物理机上安装原生的Linux,我们可以根据自身的喜好选择某种Linux Distribution,目前来说像RHEL ...

  9. 《ASP.NET Core 3框架揭秘》博文汇总

    在过去一段时间内,写了一系列关于ASP.NET Core 3相关的文章,其中绝大部分来源于即将出版的<ASP.NET Core 3框架揭秘>(博文只能算是"初稿",与书 ...

随机推荐

  1. 找到了element, 但是用getText却得到空值,取不到文本的解决办法

    最近代码中发现一些bug, 在Debug过程中发现,页面元素是被定位到了,但是用getText方法取到的却是空值.调查了一下发现,getText是否返回值和isDisplayed是否为true有关.当 ...

  2. (六)OpenStack---M版---双节点搭建---Neutron安装和配置

    ↓↓↓↓↓↓↓↓视频已上线B站↓↓↓↓↓↓↓↓ >>>>>>传送门 1.创建网络服务数据库 2.获得 admin 凭证来获取只有管理员能执行的命令的访问权限 3.创 ...

  3. nslookup命令查找域名

    了解 DNS 域名服务 熟悉使用 nslookup 查找 DNS 服务器上登记的域名,记录几次查询的结果, 及服务器的 ip. 1. 某个子域下的一部分主机的名字- IP 地址对应关系,如 flame ...

  4. 【Luogu P1972】HH的项链

    Luogu P1972 一开始非常naive随便打了个树状数组统计就交上去了,然后不出意料的爆零了-- 然后删一删改一改过了. 重点:对于区间[1,r]中重复出现的数,我们只需要关心最右边那一个是否在 ...

  5. 【Luogu P1502】窗口的星星

    Luogu P1502 题意很好理解,就是问给出的矩形套住的最大和. 但是做起来却十分麻烦. --来自疯狂爆10分的愤怒 一个比较高效的思路是--把每一个星星作为左下角向右上方拓展形成一个矩形, 拓展 ...

  6. day 14 内置函数

    复习了解: \t   输出一个制表符,协助在输出文本时,垂直方向保持对齐 \n       换行符 print(r"\n ")  #  在字符串前面加r 不会改变字符串的内容 a ...

  7. 【设计模式大法】Iterator模式

    Iterator模式 --一个一个遍历 在Java中的for语句中 i++的作用是让 i 的值在每次循环后自增1,这样就可以访问数组中的下一个元素.下下一个元素.再下下一个元素,也就实现了从头至尾逐一 ...

  8. MySQL统计各个表中的记录数

    通过下面的SQL语句可以统计出数据库的各个表中的记录数: select table_schema, table_name,table_rows from information_schema.tabl ...

  9. kubeadm配置高可用etcd集群

    操作系统为ubuntu18 kubernetes版本为v1.15.1 k8s默认在控制平面节点上的kubelet管理的静态pod中运行单个成员的etcd集群,但这不是高可用的方案. etcd高可用集群 ...

  10. ios注册通知NSNotificationCenter(一)

    作用:NSNotificationCenter是专门供程序中不同类间的消息通信而设置的. 注册通知:即要在什么地方接受消息 [[NSNotificationCenter defaultCenter]  ...