.NET原生不支持文件系统层叠,因System.IO面向物理路径且无拦截钩子;需通过IFileProvider链式包装或Stream装饰器实现加密、压缩等叠加功能。

为什么 .NET 原生不支持“文件系统层叠”
因为 System.IO 的设计是面向物理/逻辑路径的扁平抽象,所有操作最终都落到 FileStream 或 DirectoryInfo 这类具体实现上,没有预留中间拦截或装饰的钩子。你不能直接把一个“加密文件流”塞进 Directory.EnumerateFiles() —— 它只认真实路径。
真正能组合的不是“文件系统”,而是“流”和“路径解析逻辑”。加密、压缩、远程挂载这些特性,得在 Stream 层或自定义 IFileSystem 接口上做文章。
- 加密:用
CryptoStream包裹底层FileStream,但目录遍历、元数据(如LastWriteTime)仍需单独处理 - 压缩:类似,
GZipStream或DeflateStream只作用于读写内容,不改变路径语义 - 远程/虚拟路径:必须自己实现
IFileSystem(比如用Microsoft.Extensions.FileSystemGlobbing配合自定义IFileProvider)
用 IFileProvider 实现可插拔的路径抽象
IFileProvider 是 ASP.NET Core 提供的轻量级文件系统抽象,它不依赖物理磁盘,天然适合叠加行为。你可以链式包装:比如 EncryptedFileProvider → CompressedFileProvider → EmbeddedFileProvider。
关键点在于:每个包装器只重写 GetFileInfo() 和 GetDirectoryContents(),内部调用被包装的 provider,并对返回的 IFileInfo 或流做转换。
-
GetFileInfo(path)返回的IFileInfo.CreateReadStream()必须返回已加密/解密后的Stream,否则上层读取会失败 - 加密 provider 无法自动识别哪些文件该加解密 —— 你需要约定规则,比如后缀
.enc,或通过path前缀判断 - 注意时间戳、长度等元数据:加密后文件大小变化,
Length应返回解密后的内容长度(需预读或存额外元数据)
var encrypted = new EncryptedFileProvider(
new PhysicalFileProvider("C:\data"),
new AesEncryptionProvider(key)
);
Stream 装饰器组合的实际限制
你可以把 CryptoStream 和 GZipStream 套在一起,但顺序和模式必须严格匹配:比如先压缩再加密(GZipStream → CryptoStream → FileStream),解包时就得反向(CryptoStream → GZipStream)。一旦顺序错,就得到乱码或 CryptographicException: Padding is invalid。
-
CryptoStream默认使用CryptoStreamMode.Read,不可 seek —— 所以叠加后整个流变成非随机访问,FileInfo.Length无法直接反映原始内容长度 - 压缩流不保证输出确定性(尤其 LZ77 实现),所以“压缩+加密”后两次写同一文件,二进制可能不同 —— 别指望用
File.GetLastWriteTime()或哈希做一致性校验 - 别试图在
StreamWriter外层套CryptoStream:编码(如 UTF-8 BOM)和加密块边界冲突,容易丢字节
别踩坑:加密文件名 vs 加密内容
绝大多数人只加密内容,却忘了文件名本身也是敏感信息。Windows / Linux 文件系统不提供对文件名的透明加解密支持 —— Directory.GetFiles("*.txt") 在加密 provider 里查不到 secret.enc,除非你重写通配符匹配逻辑。
- 如果要加密文件名,必须在
IFileProvider层做双向映射:存储时把report.docx映射为随机字符串(如a7f2b9c1),读取时反查 - 这种映射需要持久化(比如 SQLite 表或内存字典),且要考虑并发写入冲突
- 别用简单哈希(如
MD5(filename))当密文名:可被暴力枚举;要用带 salt 的 AEAD 加密,或 HMAC + 随机 nonce
真正难的不是拼功能,是让“加密”“压缩”“远程”这些层在元数据、错误传播、缓存策略、线程安全上互相不撕扯。比如一个 CompressedEncryptedFileProvider 抛出异常时,你得决定该暴露底层 IOException 还是封装成自定义 CorruptedArchiveException —— 这个选择会影响所有上层调用方的错误处理逻辑。










