01
将项目文件变成内嵌资源
在默认情况下,我们添加到一个.NET Core项目中的静态文件并不会成为项目编译生成的程序集的内嵌资源文件。如果想将静态文件作为目标程序集的内嵌文件,我们需要修改定义当前项目的.csproj文件。具体来说,我们需要按照前面实例演示的方式在.csproj文件中添加<ItemGroup>/<EmbeddedResource>元素,并利用Include属性显式地将对应的资源文件包含进来。
图1 包含资源文件的.NET Core项目
<EmbeddedResource>的Include属性可以设置多个路径,路径之间采用分号(“;”)作为分隔符。以图1所示的目录结构为例,如果我们需要将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/gux.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>
02
读取资源文件
每个程序集都有一个清单文件(Manifest),它的一个重要作用就是记录组成程序集的所有文件成员。总的来说,一个程序集主要由两种类型的文件构成,它们分别是承载IL代码的托管模块文件和编译时内嵌的资源文件。针对图1所示的项目结构,如果我们将四个文本文件以资源文件的形式内嵌到生成的程序集(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.gux.txt
{
// Offset: 0x00000030 Length: 0x0000000C
}
虽然文件在原始的项目中具有层次化的目录结构,但是当它们成功转移到编译生成的程序集中之后,目录结构将不复存在,它们将统一存放在同一个容器中。如果我们通过Reflector打开程序集,资源文件的扁平化存储将会一目了然(如图2所示)。
为了避免命名冲突,编译器将会根据原始文件所在的路径来命名,具体的规则是“{BaseNamespace}.{Path}”,目录分隔符将统一转换成“.”,值得强调的是资源文件名称的前缀不是程序集的名称,而是我们为项目设置的基础命名空间的名称。
图2内嵌资源文件的扁平化存储
表示程序集的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.gux.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"));
}
}
03
EmbededFileProvider
在对内嵌于程序集的资源文件有了大致的了解之后,针对EmbeddedFileProvider的实现原理就很好理解了。由于内嵌于程序集的资源文件采用扁平化存储形式,所以在通过 EmbeddedFileProvider构建的文件系统中并没有目录层级的概念。我们可以认为所有的资源文件都保存在程序集的“根目录”下。
对于EmbeddedFileProvider构建的文件系统来说,它提供的IFileInfo对象总是对一个具体资源文件的描述。具体来说,这是一个EmbeddedResourceFileInfo对象,具有如下定义的EmbeddedResourceFileInfo类型由NuGet包“Microsoft.Extensions.FileProviders.Embedded”来提供。
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;
Name = name;
LastModified = lastModified;
}
public Stream CreateReadStream()
{
var stream = _assembly
.GetManifestResourceStream(_resourcePath);
if (!_length.HasValue)
{
_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 = new long?(stream.Length);
}
}
Return _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对象。
https://mp.weixin.qq.com/s/bMb_PoX51I1CO2J9mHlO0g