在《抽象的“文件系统”》中,我们通过几个简单的实例演示从编程的角度对文件系统做了初步的体验,接下来我们继续从设计的角度来进一步认识它。这个抽象的文件系统以目录的形式来组织文件,我们可以利用它读取某个文件的内容,还可以对目录或者文件实施监控并及时得到变化的通知。由于IFileProvider对象提供了针对文件系统变换的监控功能,在.NET Core下里类似的功能大都利用一个IChangeToken对象来实现,所以我们在对IFileProvider进行深入介绍之前有必要先来了解一下IChangeToken。
一、IChangeToken
从字面上理解的IChangeToken对象就是一个与某组监控数据关联的“令牌(Token)”,它能够在检测到数据改变的时候及时地对外发出一个通知。如果IChangeToken关联的数据发生改变,它的HasChanged属性将变成True。我们可以调用其RegisterChangeCallback方法注册一个在数据发生改变时可以自动执行的回调,该方法会返回一个IDisposable对象,我们通过用其Dispose方法解除注册的回调。至于IChangeToken接口的另一个属性ActiveChangeCallbacks,它表示当数据发生变化时是否需要主动执行注册的回调操作。
public interface IChangeToken
{
bool HasChanged { get; }
bool ActiveChangeCallbacks { get; }
IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
.NET Core提供了若干原生的IChangeToken实现类型,我们最常使用的是一个名为CancellationChangeToken的实现。CancellationChangeToken的实现原理很简单,它基本上就是按照如下的形式借助我们熟悉的CancellationToken对象来发送通知。
public class CancellationChangeToken : IChangeToken
{
private readonly CancellationToken _token;
public CancellationChangeToken(CancellationToken token) => _token = token;
public bool HasChanged => _token.IsCancellationRequested;
public bool ActiveChangeCallbacks => true;
public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _token.Register(callback, state);
}
除了CancellationChangeToken,有时也我们也会使用到一个名为CompositeChangeToken的实现。顾名思义,CompositeChangeToken代表由多个IChangeToken组合而成的复合型IChangeToken对象。如下面的代码片段所示,我们在调用构造函数创建一个CompositeChangeToken对象的时候,需要提供这些IChangeToken对象。对于一个CompositeChangeToken对象来说,只要组成它的任何一个IChangeToken发生改变,其HasChanged属性将会变成True,而注册的回调自然会被执行。至于ActiveChangeCallbacks属性,只要任何一个IChangeToken的同名属性返回True,该属性就会返回True。
public class CompositeChangeToken : IChangeToken
{
public bool ActiveChangeCallbacks { get; }
public IReadOnlyList<IChangeToken> ChangeTokens { get; }
public bool HasChanged { get; } public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);
public IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
我们可以直接调用IChangeToken提供的RegisterChangeCallback方法来注册在接收到数据变化通知后的回调操作,但是更常用的方式则是直接调用静态类型ChangeToken提供的如下两个OnChange方法重载来进行回调注册,这两个方法的第一个参数需要被指定为一个用来提供IChangeToken对象的Func<IChangeToken>委托。
public static class ChangeToken
{
public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer) ;
public static IDisposable OnChange<TState>(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state) ;
}
二、IFileProvider
在了解了IChangeToken是怎样一个对象之后,我们将关注转移到文件系统的核心接口IFileProvider上,该接口定义在NuGet包“Microsoft.Extensions.FileProviders.Abstractions”中。我们在《抽象的“文件系统”》做了几个简单的实例演示,它们实际上体现了文件系统承载的三个基本功能,而这三个基本功能分别体现在IFileProvider接口如下所示的三个方法中。
public interface IFileProvider
{
IFileInfo GetFileInfo(string subpath);
IDirectoryContents GetDirectoryContents(string subpath);
IChangeToken Watch(string filter);
}
三、IFileInfo
虽然文件系统采用目录来组织文件,但是不论是目录还是文件都通过一个IFileInfo对象来表示,至于具体是目录还是文件则通过IFileInfo的IsDirectory属性来确定。对于一个IFileInfo对象,我们可以通过只读属性Exists判断指定的目录或者文件是否真实存在。至于另外两个属性Name和PhysicalPath,它们分别表示文件或者目录的名称和物理路径。属性LastModified返回一个时间戳,表示目录或者文件最终一次被修改的时间。对于一个表示具体文件的IFileInfo对象来说,我们可以利用属性Length得到文件内容的字节长度。如果我们希望读取文件的内容,可以借助于CreateReadStream方法返回的Stream对象来完成。
public interface IFileInfo
{
bool Exists { get; }
bool IsDirectory { get; }
string Name { get; }
string PhysicalPath { get; }
DateTimeOffset LastModified { get; }
long Length { get; } Stream CreateReadStream();
}
IFileProvider接口的GetFileInfo方法会根据指定的路径得到表示所在文件的IFileInfo对象。换句话说,虽然一个IFileInfo对象可以用于描述目录和文件,但是GetFileInfo方法的目的在于得到指定路径返回的文件而不是目录(我个人不太认同这种令人产生歧义的API设计)。一般来说,不论指定的文件是否存在,该方法总会返回一个具体的IFileInfo对象,因为目标文件的存在与否是由该对象的Exists属性来确定的。
四、IDirectoryContents
如果希望得到某个目录的内容,比如需要查看多少文件或者子目录包含在这个目录下,我们可以调用IFileProvider对象的GetDirectoryContents方法并将所在目录的路径作为参数。目录内容通过该方法返回的IDirectoryContents对象来表示。如下面的代码片段所示,一个IDirectoryContents对象实际上是一组IFileInfo对象的集合,组成这个集合的所有IFileInfo自然就是对包含在这个目录下的所有文件和子目录的描述。和GetFileInfo方法一样,不论指定的目录是否存在,GetDirectoryContents方法总是会返回一个具体的IDirectoryContents对象,它的Exists属性会帮助我们确定指定目录是否存在。
public interface IDirectoryContents : IEnumerable<IFileInfo>
{
bool Exists { get; }
}
五、监控目录或者文件更新
如果我们希望监控IFileProvider所在目录或者文件的变化,我们可以调用它的Watch方法,当然前提是对应的IFileProvider对象提供了这样的监控功能。这个方法接受一个字符串类型的参数filter,我们可以利用这个参数指定一个针对“文件匹配模式(File Globing Pattern)”表达式(以下简称Globing Pattern表达式)来筛选需要监控的目标目录或文件。
Globing Pattern表达式比正则表达式简单多了,它只包含“*”一种“通配符”,如果硬说它包含两种通配符的话,那么另一个通配符是“**”。Globing Pattern表达式体现为一个文件路径,其中“*”代表所有不包括路径分隔符(“/”或者“\”)的所有字符,而“**”则代表包含路径分隔符在内的所有字符。下表给出了几个典型的Globing Pattern表达式和它们代码的文件匹配语义。
Globing |
匹配的文件 |
src/foobar/foo/settings.* |
子目录“src/foobar/foo/”(不含其子目录)下名为“settings”的所有文件,比如settings.json、settings.xml和settings.ini等。 |
src/foobar/foo/*.cs |
子目录“src/foobar/foo/”(不含其子目录)下的所有.cs文件。 |
src/foobar/foo/*.* |
子目录“src/foobar/foo/”(不含其子目录)下所有文件。 |
src/**/*.cs |
子目录“src”(含其子目录)下的所有.cs文件。 |
一般来说,不论是调用IFileProvider对象的GetFileInfo或GetDirectoryContents方法所指定的目标文件或目录的路径,还是调用Watch方法指定的筛选表达式,都是一个针对当前IFileProvider对象映射根目录的相对路径。指定的这个路径可以采用“/”字符作为前缀,但是这个前缀是不必要的。换句话说,如下所示的这两组程序是完全等效的。
路径不包含前缀“/”
var dirContents = fileProvider.GetDirectoryContents("foobar");
var fileInfo = fileProvider.GetFileInfo("foobar/foobar.txt");
var changeToken = fileProvider.Watch("foobar/*.txt");
路径包含前缀“/”
var dirContents = fileProvider.GetDirectoryContents("/foobar");
var fileInfo = fileProvider.GetFileInfo("/foobar/foobar.txt");
var changeToken = fileProvider.Watch("/foobar/*.txt");
总的来说,以IFileProvider对象为核心的文件系统在设计上看是非常简单的。除了IFileProvider接口之外,文件系统还涉及到其他一些对象,比如IDirectoryContents、IFileInfo和IChangeToken等,下图所示的UML展示了这些接口以及它们之间的关系。
[ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”
[ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计
[ASP.NET Core 3框架揭秘] 文件系统[3]:物理文件系统
[ASP.NET Core 3框架揭秘] 文件系统[4]:程序集内嵌文件系统