NetCore 入门 (二) : 文件系统
1. Quick Start
ASP.NET Core应用具有很多读取文件的场景,如读取配置文件、静态Web资源文件(js/css/image)、MVC应用的View文件、以及直接编译到程序集中的内嵌资源文件。这些文件的读取都需要一个IFileProvider
对象。
IFileProvider
对象构建了一个抽象的文件系统,不仅提供了统一的API来读取各种类型的文件,还能及时监控目标文件的变化。
1.1 安装NuGet包
Microsoft.Extensions.FileProviders.Abstractions // 抽象依赖包
Microsoft.Extensions.FileProviders.Embedded // 嵌入式文件系统
Microsoft.Extensions.FileProviders.Physical // 物理文件系统
Microsoft.Extensions.FileProviders.Composite // 复合型文件系统
1.2 示例1 - 遍历目录
现有文件目录如下所示:
F:/root
├─ dir1/
│ ├─ footbar/
│ │ ├─ bar.txt
│ │ └─ foo.txt
│ └─ baz.txt
├─dir2/
│ └─ quz.txt
├─ bzx.txt
└─ coding.txt
现在以F:/root
为根目录,构建物理文件系统。
using (var fileProvider = new PhysicalFileProvider(@"F:\root"))
{
Console.WriteLine("获取根目录");
IDirectoryContents directoryContents = fileProvider.GetDirectoryContents("/");// 获取根目录
foreach (var item in directoryContents)
{
Console.WriteLine($"Name:{item.Name}, IsDirectory:{item.IsDirectory}, {item.PhysicalPath}");
}
Console.WriteLine("获取目录dir1");
directoryContents = fileProvider.GetDirectoryContents("/dir1");// 获取目录dir1
foreach (var item in directoryContents)
{
Console.WriteLine($"Name:{item.Name}, IsDirectory:{item.IsDirectory}, {item.PhysicalPath}");
}
}
输出结果如下:
获取根目录
Name:bzx.txt, IsDirectory:False, F:\root\bzx.txt
Name:coding.txt, IsDirectory:False, F:\root\coding.txt
Name:dir1, IsDirectory:True, F:\root\dir1
Name:dir2, IsDirectory:True, F:\root\dir2
获取目录dir1
Name:baz.txt, IsDirectory:False, F:\root\dir1\baz.txt
Name:footbar, IsDirectory:True, F:\root\dir1\footbar
1.3 示例2 - 读取文件内容
using (var fileProvider = new PhysicalFileProvider(@"F:\root"))
{
IFileInfo fileInfo = fileProvider.GetFileInfo("dir1/baz.txt");
string content = "";
using (var stream = fileInfo.CreateReadStream())
{
byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
content = Encoding.Default.GetString(buffer);
}
Console.WriteLine(content);
}
1.4 示例3 - 监控文件变化
var fileProvider = new PhysicalFileProvider(@"F:\root");
ChangeToken.OnChange(() => fileProvider.Watch("dir1/baz.txt"), callback);
void callback()
{
IFileInfo fileInfo = fileProvider.GetFileInfo("dir1/baz.txt");
string content = "";
using (var stream = fileInfo.CreateReadStream())
{
byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
content = Encoding.Default.GetString(buffer);
}
Console.WriteLine(content);
}
int i = 0;
while (i++ < 10)
{
File.WriteAllText(@"F:\root\dir1\baz.txt", DateTime.Now.ToString());
await Task.Delay(1000);
}
输出结果:
2020/6/5 16:23:24
2020/6/5 16:23:24
2020/6/5 16:23:25
2020/6/5 16:23:26
2020/6/5 16:23:27
2020/6/5 16:23:28
2020/6/5 16:23:29
2020/6/5 16:23:30
2020/6/5 16:23:31
2020/6/5 16:23:32
2020/6/5 16:23:33
2. 模型解析
2.1 Globbing Pattern
Globbing Pattern表达式体现为一个文件路径,用于筛选目标目录或文件。Globbing Pattern表达式只包含2个通配符:
*
: 代表所有不包含路径分隔符的所有字符。**
: 代表路径分隔符在内的所有字符。
下面列举几种常见的Globbing Pattern表达式:
Globbing Pattern表达式 | 匹配的文件 |
---|---|
src/foobar/foo/settings.* | 目录“src/foobar/foo/”下所有名为“settings”的文件,如settings.json、settigns.ini、settings.xml等 |
src/foobar/foo/*.cs | 目录“src/foobar/foo/”下所有.cs文件 |
src/foobar/foo/ * .* | 目录“src/foobar/foo/”下的所有文件 |
src/**/*.cs | 目录"src"及其子目录下的所有.cs文件 |
2.2 核心接口
IFileProvider
IFileProvider
是文件系统的核心接口,该接口定义了3个方法,体现了文件系统的3个基本功能。
public interface IFileProvider
{
// 遍历目录
IDirectoryContents GetDirectoryContents(string subpath);
// 读取文件
IFileInfo GetFileInfo(string subpath);
// 监控文件变化
IChangeToken Watch(string filter);
}
Watch
方法监控目录或文件的变化。该方法接收一个Globbing Pattern字符串参数filter
,用来筛选需要监控的目标文件。
IFileInfo
public interface IFileInfo
{
bool Exists { get; }
long Length { get; }
string PhysicalPath { get; }
string Name { get; }
DateTimeOffset LastModified { get; }
bool IsDirectory { get; }
Stream CreateReadStream();
}
- 虽然文件系统采用目录组织文件,但不论文件还是目录都都通过一个
IFileInfo
对象表示。至于具体是目录还是文件,要通过IsDirectory
属性来判断。 - 一般来说,不论指定的文件是否存在,
GetFileInfo
方法总是返回一个具体的IFileInfo
对象。目标文件的存在与否由Exists
属性决定。 - 可以借助
CreateReadStream
方法读取文件的内容。
IDirectoryContents
public interface IDirectoryContents : IEnumerable<IFileInfo>
{
bool Exists { get; }
}
一个IDirectoryContents
对象实际上是一组IFileInfo
对象的集合。和GetFileInfo
方法一样,不论指定的目录是否存在,GetDirectoryContents
方法总是返回一个具体的IDirectoryContents
对象,它的Exists
属性可以确定目录是否存在。
IChangeToken
public interface IChangeToken
{
// Gets a value that indicates if a change has occurred.
bool HasChanged { get; }
// Indicates if this token will pro-actively raise callbacks. If false, the token
// consumer must poll Microsoft.Extensions.Primitives.IChangeToken.HasChanged to
// detect changes.
bool ActiveChangeCallbacks { get; }
IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
IChangeToken
对象是一个监控数据的“令牌”,它能够在数据发生变更时及时对外发出一个通知。
我们可以调用RegisterChangeCallback
方法注册一个回调函数,在数据发生变化时自动做出响应。该函数的返回值是一个IDisposable
对象,调用Dispose
方法可以解除注册的回调。
2.3 ChangeToken
CancellationChangeToken
.Net Core提供了若干原生的IChangeToken
接口实现,我们最常用的是CancellationChangeToken
。它的实现原理很简单,就是借助System.Threading.CancellationToken
对象来发送通知的。
public class CancellationChangeToken : IChangeToken
{
private readonly CancellationToken _token;
// 参数:
// cancellationToken:
// The System.Threading.CancellationToken
public CancellationChangeToken(CancellationToken cancellationToken)
=> _token = cancellationToken;
public bool ActiveChangeCallbacks { get; } = true;
public bool HasChanged { get; } = _token.IsCancellationRequested;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
=> _token.Register(callback, state);
}
CompositeChangeToken
public class CompositeChangeToken : IChangeToken
{
public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);
public IReadOnlyList<IChangeToken> ChangeTokens { get; }
public bool HasChanged { get; }
public bool ActiveChangeCallbacks { get; }
public IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
CompositeChangeToken
代表由多个IChangeToken
组合而成的复合型IChangeToken
对象, 在创建CompositeChangeToken
时需要提供这些IChangeToken
。- 对于一个
CompositeChangeToken
对象来说,只要组成它的任何一个IChangeToken
发生改变,其HasChanged
属性就返回True, 并调用其注册的回调函数。 - 任何一个
IChangeToken
的ActiveChangeCallbacks
属性返回True, 则ActiveChangeCallbacks
属性返回True。
ChangeToken
我们可以使用IChangeToken
的RegisterChangeCallback
方法来注册回调函数。但是这种方法存在一点不足:一个IChangeToken
对象仅能发送一次通知。也就是说,如果想要连续不断的检测文件变化,就必须多次调用Watch
方法并注册回调函数。
好在ChangeToken
类帮我们解决了这个问题:OnChange
方法会在一个IChangeToken
对象发送通知后再次添加监控。
public static class ChangeToken
{
public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer);
public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer,
ction<TState> changeTokenConsumer, TState state);
}
使用示例:
ChangeToken.OnChange(() => fileProvider.Watch("dir1/*.txt"), ()=>{ ... });
3. 物理文件系统
3.1 PhysicalFileProvider
物理文件系统是由PhysicalFileProvider
构建的。一个PhysicalFileProvider
总是映射到一个具体的物理目录上,并把该目录作为文件系统的根目录。
public class PhysicalFileProvider : IFileProvider, IDisposable
{
public PhysicalFileProvider(string root);
public PhysicalFileProvider(string root, ExclusionFilters filters);
public string Root { get; }
public IDirectoryContents GetDirectoryContents(string subpath);
public IFileInfo GetFileInfo(string subpath);
public IChangeToken Watch(string filter);
...
}
3.2 获取文件
PhysicalFileInfo
如果存在指定的物理文件,则GetFileInfo
方法返回一个PhysicalFileInfo
对象。该对象实际上是对System.IO.FileInfo
的封装。
public class PhysicalFileInfo : IFileInfo
{
public PhysicalFileInfo(FileInfo info);
...
}
NotFoundFileInfo
PhysicalFileProvider
会将以下场景视为“目标文件不存在”,并让GetFileInfo
方法返回NotFoundFileInfo
对象。
- 确实不存在一个物理文件与指定的路径相匹配。
- 指定的是绝对路径。
- 路径指向一个隐藏文件。
public class NotFoundFileInfo : IFileInfo
{
public bool Exists { get; } = false;
...
}
PhysicalDirectoryInfo
如果指向的路径代表一个目录,则GetFileInfo
方法返回PhysicalDirectoryInfo
对象。该对象是对System.IO.DirectoryInfo
的封装。
public class PhysicalDirectoryInfo : IFileInfo
{
public PhysicalDirectoryInfo(DirectoryInfo info);
...
}
3.3 遍历目录
PhysicalDirectoryContents
调用GetDirectoryContents
方法时,如果路径指向一个存在的目录,则返回PhysicalDirectoryContents
对象。
public class PhysicalDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>
{
public PhysicalDirectoryContents(string directory);
public bool Exists { get; }
...
}
NotFoundDirectoryContents
如果路径指向一个不存在的目录,或者是一个绝对路径,则返回NotFoundDirectoryContents
对象。
public class NotFoundDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>
{
public static NotFoundDirectoryContents Singleton { get; }
public bool Exists { get; } = false;
...
}
3.4 文件监控
PhysicalFilesWatcher
PhysicalFileProvider利用PhysicalFilesWatcher
来实现文件监控。文件或目录的任何变化(创建、修改、重命名和删除)都会实时地反映到IChangeToken
对象上。
public class PhysicalFilesWatcher : IDisposable
{
public PhysicalFilesWatcher(string root, FileSystemWatcher fileSystemWatcher, bool pollForChanges);
public IChangeToken CreateFileChangeToken(string filter);
...
}
从构造函数可以看出,PhysicalFilesWatcher是利用System.IO.FileSystemWatcher
来实现文件监控功能的。
4. 嵌入式文件系统
4.1 内嵌资源介绍
4.1.1 添加内嵌资源
如果需要把资源内嵌至程序集中,就需要修改当前项目的.csproj
文件。通过添加<EmbeddedResource>
,把对应的文件包含进来。
现将如下目录结构的文件内嵌至程序集中(root位于项目的根目录下):
root
├─ dir1/
│ ├─ footbar/
│ │ └─ bar.txt
│ └─ baz.txt
├─dir2/
│ └─ quz.txt
└─ coding.txt
修改项目的.csproj
文件:
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup>
<EmbeddedResource Include="root\**" Exclude=""/>
</ItemGroup>
...
</Project>
<EmbeddedResource>
具有两个属性,每个属性的值都可以使用Globbing Pattern表达式:
- Include: 添加内嵌资源文件
- Exclude: 排除不符合要求的文件。
4.1.2 文件命名空间
每个程序集都有一个清单文件(Manifest),其作用是记录组成程序集的所有文件。如果将上述文件文件内嵌到程序集中,清单文件将采用如下方式来记录它们。
.mresource public App.root.dir1.footbar.bar.txt
{
// Offset: 0x00000000 Length: 0x0000000C
}
.mresource public App.root.dir1.baz.txt
{
// Offset: 0x00000020 Length: 0x0000000C
}
.mresource public App.root.dir2.quz.txt
{
// Offset: 0x00000030 Length: 0x0000000C
}
.mresource public App.root.coding.txt
{
// Offset: 0x00000050 Length: 0x0000000C
}
虽然文件在原始项目中具有层次化的目录结构,但是在编译生成的程序集中,目录结构将不复存在,所有的文件按照命名空间的形式存放在同一个容器中。在生成程序集的过程中,编译器会按照一定的规则对文件进行重命名。
其规则是:{BaseNamespace}.{Path}
,目录分隔符(“/”)将替换成“.”。
::: tip BaseNamespace
这里的BaseNamespace不是程序集的名称,而是项目设置的默认命名空间的名称。
:::
4.1.3 读取资源文件
表示程序集的Assembly
对象定义了如下几个方法,用来操作内嵌资源文件。
public abstract class Assembly
{
// 获取记录在程序集Manifest中的资源文件名
public virtual string[] GetManifestResourceNames();
// 获取指定资源文件的描述信息
public virtual ManifestResourceInfo? GetManifestResourceInfo(string resourceName);
// 返回一个读取文件内容的stream对象
public virtual Stream? GetManifestResourceStream(string name);
...
}
4.2 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);
}
::: tip baseNamespace
构建EmbeddedFileProvider
时如果没有指定baseNamespace
, 则采用程序集的名称作为命名空间。
:::
示例:
Console.WriteLine("================默认baseNamespace");
var provider = new EmbeddedFileProvider(Assembly.GetExecutingAssembly());
var contents = provider.GetDirectoryContents("/");
foreach (var info in contents)
{
Console.WriteLine(info.Name);
}
Console.WriteLine("================设置baseNamespace");
provider = new EmbeddedFileProvider(Assembly.GetExecutingAssembly(), "FileProviderTutorial.root.dir1");
contents = provider.GetDirectoryContents("/");
foreach (var info in contents)
{
Console.WriteLine(info.Name);
}
输出:
================默认baseNamespace
root.bzx.txt
root.dir1.baz.txt
root.dir1.footbar.bar.txt
root.dir2.quz.txt
================设置baseNamespace
baz.txt
footbar.bar.txt
4.3 ManifestEmbeddedFileProvider
与EmbeddedFileProvider
的功能基本一致,不同点在于:它根据清单来重建嵌入文件的原始路径。也就是说,ManifestEmbeddedFileProvider
构建的文件系统是有目录结构的。
public class ManifestEmbeddedFileProvider : IFileProvider
{
public ManifestEmbeddedFileProvider(Assembly assembly);
public ManifestEmbeddedFileProvider(Assembly assembly, string root);
public IDirectoryContents GetDirectoryContents(string subpath);
public IFileInfo GetFileInfo(string subpath);
public IChangeToken Watch(string filter);
}
5 其他文件系统
5.1 复合型文件系统
5.1.1 CompositeFileProvider
CompositeFileProvider
代表由多个IFileProvider
构建的复合型文件系统。当创建一个CompositeFileProvider
对象实例时,需要提供一组IFileProvider
对象。
public class CompositeFileProvider : IFileProvider
{
public CompositeFileProvider(params IFileProvider[] fileProviders);
public CompositeFileProvider(IEnumerable<IFileProvider> fileProviders);
public IEnumerable<IFileProvider> FileProviders { get; }
public IDirectoryContents GetDirectoryContents(string subpath);
public IFileInfo GetFileInfo(string subpath);
public IChangeToken Watch(string pattern);
}
5.1.2 GetFileInfo
当调用GetFileInfo
方法时,它会遍历这些IFileProvider
对象,直到找到一个与路径相匹配的文件。如果所有的IFileProvider
都不能提供这个文件,则返回NotFoundFileInfo
对象。
由于遍历的顺序取决于构建CompositeFileProvider
对象时提供的IFileProvider
的顺序,所以如果对IFileProvider
具有优先级的要求,应该将优先级高的IFileProvider
对象放在前面。
5.1.3 CompositeDirectoryContents
GetDirectoryContents
方法返回一个复合型的IDirectoryContents
,即CompositeDirectoryContents
。如果多个IFileProvider
中存在路径相同的文件,与GetFileInfo
一样,总是从优先提供的IFileProvider
中提取。
public class CompositeDirectoryContents : IDirectoryContents, IEnumerable<IFileInfo>
{
public CompositeDirectoryContents(IList<IFileProvider> fileProviders, string subpath);
public bool Exists { get; }
}
5.1.4 CompositeChangeToken
Watch
方法也返回一个复合型对象,即CompositeChangeToken
。这个对象由所有的IFileProvider
调用Watch
方法来构建,所以它能监控所有的IFileProvider
对象对应的文件系统。
public class CompositeChangeToken : IChangeToken
{
public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);
...
}
5.2 空文件系统
NullFileProvider
代表一个不包含任何内容的空文件系统。
public class NullFileProvider : IFileProvider
{
public IDirectoryContents GetDirectoryContents(string subpath) => new NotFoundDirectoryContents();
public IFileInfo GetFileInfo(string subpath) => new NotFoundFileInfo(subpath);
public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
}
GetDirectoryContents
返回NotFoundDirectoryContents
对象。GetFileInfo
返回NotFoundFileInfo
对象。Watch
返回NullChangeToken
对象。