.NET 物理文件提供程序深入解析:Microsoft.Extensions.FileProviders.Physical 的查找、监视与轮询机制
2026/9/20 16:19:00 网站建设 项目流程
  • 语言运行时
  • 标准库
  • JIT编译
  • 编译器

【免费下载链接】runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

项目地址:https://gitcode.com/GitHub_Trending/runtime6/runtime
点击查看免费下载

Microsoft.Extensions.FileProviders.Physical是 .NET 运行时仓库中负责"从磁盘查找文件、并监视磁盘变化"的官方实现。它以PhysicalFileProvider为核心类型,向应用提供统一的IFileProvider抽象:既可以基于FileSystemWatcher实时响应文件变更,也可以在FileSystemWatcher失效的场景(如挂载盘、网络共享、WebAssembly/移动端)下自动退化为按固定间隔轮询。读完本文,你将掌握该组件的完整 API 面、路径安全与排除过滤规则、两种变更监视机制的切换开关,以及其底层源码级的工作原理,可直接用于构建配置热重载、静态资源监视、模板引擎文件系统等真实场景。

一、组件定位:IFileProvider 抽象的物理实现

在 .NET 的扩展体系中,文件访问被抽象为IFileProvider接口,从而让上层框架(配置系统、静态文件中间件、Razor 视图引擎等)不依赖具体文件系统形态。Microsoft.Extensions.FileProviders.Physical正是该抽象的"本地磁盘实现":

  • 通过GetFileInfo(subpath)在磁盘上定位单个文件;
  • 通过GetDirectoryContents(subpath)枚举目录内容;
  • 通过Watch(filter)监视文件/目录变化并返回IChangeToken

正如该组件 README 所述,它的核心价值在于"从磁盘查找文件,并且既可以基于FileSystemWatcher、也可以基于轮询来监视磁盘变化"。它的公开 API 面定义在 ref/Microsoft.Extensions.FileProviders.Physical.cs,包含以下公开类型:

类型职责
PhysicalFileProvider面向应用的入口,实现IFileProvider,负责查找与监视
PhysicalFileInfo单个物理文件的IFileInfo实现
PhysicalDirectoryInfo单个物理目录的IFileInfo+IDirectoryContents实现
PhysicalDirectoryContents目录内容枚举实现(IDirectoryContents
PhysicalFilesWatcher底层文件系统监视器,封装FileSystemWatcher与轮询逻辑
PollingFileChangeToken针对单个文件/目录的轮询变更令牌
PollingWildCardChangeToken针对 glob 通配符模式的轮询变更令牌
ExclusionFilters文件/目录排除过滤规则([Flags]枚举)

该程序集以 NuGet 包Microsoft.Extensions.FileProviders.Physical的形式对外发布,是 .NET 通用主机(Generic Host)配置热重载、ASP.NET Core 静态文件与 Razor 热编译的基础设施之一。

二、快速上手:构造与三个核心方法

1. 构造函数与 Root 规则

PhysicalFileProvider提供两个构造函数(见 PhysicalFileProvider.cs):

public PhysicalFileProvider(string root); public PhysicalFileProvider(string root, ExclusionFilters filters);

关键约束:

  • root必须是绝对路径,否则构造函数抛出ArgumentException(源码通过Path.IsPathRooted校验);
  • root对应的目录不要求存在——即使目录尚未创建,也可以创建 provider 并注册监视,底层会等待目录出现;
  • 内部通过Path.GetFullPath规范化,并用PathUtils.EnsureTrailingSlash统一补上结尾分隔符,保证后续路径比较时"只匹配完整目录名"。

默认构造使用ExclusionFilters.Sensitive过滤规则(详见下文第四节)。

2. GetFileInfo:定位单个文件

IFileInfo info = provider.GetFileInfo("wwwroot/index.html"); if (info.Exists) { using Stream stream = info.CreateReadStream(); // 读取内容... }

GetFileInfo将相对路径直接映射到物理目录(见 PhysicalFileProvider.cs),其行为规则包括:

  • 返回的IFileInfo调用方必须检查Exists属性,因为失败场景返回的是NotFoundFileInfo而非异常;
  • 允许以/\开头的相对路径(会TrimStart去掉前导分隔符);
  • 拒绝绝对路径Path.IsPathRooted时返回 NotFound);
  • 拒绝包含非法路径字符的输入(PathUtils.HasInvalidPathChars);
  • 拒绝"逃逸到 root 之外"的路径(如../);
  • 命中排除规则的文件同样返回NotFoundFileInfo

PhysicalFileInfo包装FileInfo(见 PhysicalFileInfo.cs),其CreateReadStream()使用FileShare.ReadWrite打开流,并采用FileOptions.Asynchronous | FileOptions.SequentialScan、bufferSize 设为 1(避免 FileStream 分配内部缓冲)来适配流式读取。

3. GetDirectoryContents:枚举目录

IDirectoryContents contents = provider.GetDirectoryContents("wwwroot"); if (contents.Exists) { foreach (IFileInfo entry in contents) { Console.WriteLine($"{entry.Name} (IsDirectory={entry.IsDirectory})"); } }

目录枚举基于DirectoryInfo.EnumerateFileSystemInfos()惰性展开(见 PhysicalDirectoryInfo.cs),并在展开过程中应用排除过滤。目录不存在、路径非法、绝对路径或目录遍历异常时返回NotFoundDirectoryContents.Singleton。对于目录条目,Length恒为 -1、IsDirectory恒为 true,CreateReadStream()会抛出InvalidOperationException

三、路径安全:如何杜绝目录穿越

PhysicalFileProvider对"路径逃逸"做了两层防护(见 PathUtils.cs 与 PhysicalFileProvider.cs):

  1. 语法层校验PathNavigatesAboveRootStringTokenizer按分隔符逐段解析路径,遇到..时深度减一,一旦深度为 -1 立即判定越界;同时忽略.与空段。
  2. 物理层校验IsUnderneathRoot将合并后的完整路径与RootOrdinalIgnoreCase前缀比较(注意 Root 已补结尾斜杠),防止通过C:\root2这类前缀欺骗绕过检查。

两条路径中任一条失败,都会让GetFileInfo返回 NotFound、让Watch返回NullChangeToken。测试用例GetFileInfoReturnsNotFoundFileInfoForRelativePathAboveRootPathGetFileInfoReturnsNotFoundFileInfoForRelativePathThatNavigatesAboveRoot等在 PhysicalFileProviderTests.cs 中覆盖了这些边界。

四、ExclusionFilters:排除敏感文件

ExclusionFilters是一个[Flags]枚举(见 ExclusionFilters.cs),控制哪些文件/目录从查找与监视结果中被排除:

成员含义
None0不排除任何文件
DotPrefixed0x0001名称以.开头(如.gitignore.env
Hidden0x0002设置了FileAttributes.Hidden属性
System0x0004设置了FileAttributes.System属性
Sensitive7等价于DotPrefixed \| Hidden \| System,为默认值

由于是位标志,可以自由组合,例如ExclusionFilters.Hidden | ExclusionFilters.System表示仅排除隐藏与系统文件、但保留.dot文件。测试 ExclusionFilterTests.cs 与用例GetFileInfoReturnsNotFoundFileInfoForHiddenFileGetFileInfoReturnsFileInfoWhenExclusionDisabled验证了各组合的行为。该过滤同时作用于GetFileInfoGetDirectoryContents与底层 watcher 的事件分发。

五、变更监视的两种机制:FileSystemWatcher 与轮询

这是该组件最核心的设计。Watch(filter)返回的IChangeToken在文件/目录被新增、修改或删除时触发。触发机制有两种:

1. 基于 FileSystemWatcher(默认)

默认情况下,PhysicalFileProvider使用FileSystemWatcher监听事件(见 PhysicalFileProvider.cs 的说明)。PhysicalFilesWatcher订阅了CreatedChangedRenamedDeletedError五个事件,Renamed事件还会递归通知被重命名目录下的所有条目(见 PhysicalFilesWatcher.cs)。

2. 基于轮询(Polling)

FileSystemWatcher在部分场景下无效:例如挂载盘(mounted drives)、网络共享、某些容器/WSL 路径。此时需要轮询。切换方式有两种:

方式 A:环境变量全局开启

设置环境变量DOTNET_USE_POLLING_FILE_WATCHER"1""true"(不区分大小写)即可,见 PhysicalFileProvider.cs:

# Linux / macOS export DOTNET_USE_POLLING_FILE_WATCHER=1 dotnet run # Windows (PowerShell) $env:DOTNET_USE_POLLING_FILE_WATCHER="1" dotnet run

方式 B:通过属性编程式控制

var provider = new PhysicalFileProvider("/data/app") { UsePollingFileWatcher = true, UseActivePolling = true, };

两个属性的语义(见 PhysicalFileProvider.cs):

  • UsePollingFileWatcher:是否使用轮询来判断文件变化。默认值由DOTNET_USE_POLLING_FILE_WATCHER决定;
  • UseActivePolling:仅在UsePollingFileWatcher为 true 时生效。为 true 时,Watch返回的 tokenActiveChangeCallbacks为 true,主动通过定时器回调触发变更;为 false 时 token 是被动的,调用方必须自行轮询HasChanged

平台自动回退:在 Browser、WASI、iOS(非 Mac Catalyst)、tvOS 平台,FileSystemWatcher不受支持,CreateFileWatcher会自动把两个属性都置为 true 并跳过 FileSystemWatcher(见 PhysicalFileProvider.cs);若在构造PhysicalFilesWatcher时显式传入 FileSystemWatcher,这些平台会直接抛出PlatformNotSupportedException。属性还受时序约束:一旦底层 watcher 已被初始化,再修改UsePollingFileWatcher会抛出InvalidOperationException

3. 轮询令牌的底层原理

  • PollingFileChangeToken(单文件/目录):默认每4 秒轮询一次(PhysicalFilesWatcher.DefaultPollingInterval = TimeSpan.FromSeconds(4)),通过比较FileSystemInfo.LastWriteTimeUtc判断变化(见 PollingFileChangeToken.cs)。文件不存在时回退检查同路径的DirectoryInfo;瞬时 IO 异常按"无变化"处理,下轮重试。一旦HasChanged变为 true 就永远为 true,token不可复用
  • PollingWildCardChangeToken(glob 通配符):用Matcher执行模式匹配,对匹配到的文件集合(路径 + 最后写入时间)计算SHA-256 哈希,两次扫描哈希不同即判定变化(见 PollingWildCardChangeToken.cs),因此能捕获"新增文件"这类单个文件 token 无法感知的变化;扫描失败(如网络盘掉线)同样按无变化处理并下轮重试。
  • 主动轮询时,PhysicalFilesWatcher通过一个NonCapturingTimer周期驱动所有已注册轮询 token(RaiseChangeEvents,见 PhysicalFilesWatcher.cs),IO 异常被捕获并保留 token 等待下次轮询。

六、Watch 与 glob 模式

Watch(filter)接受 glob 通配符模式(由Microsoft.Extensions.FileSystemGlobbing.Matcher解释,见 PhysicalFileProvider.cs):

// 监视根目录下所有 .cs 文件(递归子目录) IChangeToken token1 = provider.Watch("**/*.cs"); // 监视根目录下所有文件 IChangeToken token2 = provider.Watch("*.*"); // 监视子目录下所有 .cshtml IChangeToken token3 = provider.Watch("subDirectory/**/*.cshtml"); // 监视单个具体文件 IChangeToken token4 = provider.Watch("appsettings.json");

规则与边界:

  • 监视对象不要求已存在——文件/目录可以稍后创建,这正是热重载场景的基础;
  • 模式按相对 root 解释,前导/\会被去除;
  • 非法 filter 字符(含非法文件名字符以外的*|?之外字符,见PathUtils.GetInvalidFilterChars)、绝对路径、越界路径均返回NullChangeToken.Singleton
  • *的模式或目录路径走通配符 token 路径,否则走单文件 token 路径(见 PhysicalFilesWatcher.cs);
  • 单文件 token 与通配符 token 在PhysicalFilesWatcher内部各自维护ConcurrentDictionary_filePathTokenLookup/_wildcardTokenLookup),相同模式复用同一个CancellationChangeToken(测试TokenIsSameForSamePath验证了这一点)。

七、底层 PhysicalFilesWatcher 的工程细节

PhysicalFilesWatcher是整个监视机制的中枢(见 PhysicalFilesWatcher.cs),有几点值得注意的实现细节:

  • 懒启用与自动关闭:只有注册了至少一个 token 才启用FileSystemWatcher.EnableRaisingEvents;所有 token 消耗完毕后自动关闭(TryDisableFileSystemWatcher),避免无谓的系统资源占用。
  • 递归监视的动态开关IncludeSubdirectories仅在存在需要子目录的 token 时才开启(如模式含/**,或 watcher 监视的是 root 的祖先目录),避免在 Linux 上为每个子目录创建 inotify 描述符——这是PollingFileProviderShouldntConsumeINotifyInstances测试所守护的性能点。
  • root 尚未存在:当 root 目录不存在时,PendingCreationWatcher会向上找到最近的已存在祖先目录,用非递归 watcher 逐级等待目录链创建,root 出现后再启用主 watcher,并补扫已存在的条目(ReportExistingWatchedEntries)以覆盖监视空窗期。
  • 错误处理与自我修复InternalBufferOverflowException(缓冲溢出、事件丢失)与DirectoryNotFoundException(目录被删/移动)会立即通知所有 token 并重试;同类型同错误码的重复错误会被去重,防止不可监视文件系统(如网络盘)导致的取消-重建死循环(IsSameError,见 PhysicalFilesWatcher.cs)。
  • 构造PhysicalFilesWatcher时,若pollForChanges为 false 但未提供 FileSystemWatcher,会抛出ArgumentNullException;传入的 FileSystemWatcher 路径必须与 root 有祖先/后代关系,否则抛出ArgumentException

八、典型应用场景

结合以上机制,PhysicalFileProvider的典型用法包括:

  1. 配置热重载:对appsettings.json注册Watch,结合IChangeToken.RegisterChangeCallback在文件变更时重新加载配置——这也是DOTNET_USE_POLLING_FILE_WATCHER环境变量在容器、CI 与挂载卷环境中被广泛使用的场景。
  2. 静态资源监视:Web 宿主在开发模式下用它对wwwroot建立文件提供程序,实现静态文件的即时生效与目录浏览。
  3. 模板/内容引擎:将磁盘目录作为内容源,统一通过IFileProvider接口向业务层暴露,便于后续替换为嵌入式(EmbeddedFileProvider)或内存(ManifestEmbeddedFileProvider)实现。
  4. 无法使用 FileSystemWatcher 的平台:在 WASM/移动端自动启用轮询,保证跨平台行为一致。

读者若希望深入实现细节,可重点阅读 PhysicalFileProvider.cs、PhysicalFilesWatcher.cs 两个核心文件,并以 PhysicalFileProviderTests.cs、PhysicalFilesWatcherTests.cs、PollingWildCardChangeTokenTest.cs(内含TestClockMockFileSystemWatcher等测试基建)作为行为契约参考。

九、小结

Microsoft.Extensions.FileProviders.Physical以极小的 API 表面完成了"查找 + 监视"两大职责:PhysicalFileProvider提供GetFileInfo/GetDirectoryContents/Watch三个核心入口,ExclusionFilters提供灵活的排除策略,而PhysicalFilesWatcherFileSystemWatcher与 4 秒间隔轮询之间自动或按需切换,并内建了路径越界防护、子目录监视优化、目录缺失恢复与错误自我修复等工程细节。理解它的机制,无论是排查热重载失效、在挂载卷上开启轮询,还是评估自定义文件系统实现,都能有的放矢。

  • 语言运行时
  • 标准库
  • JIT编译
  • 编译器

【免费下载链接】runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

项目地址:https://gitcode.com/GitHub_Trending/runtime6/runtime
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询