Sparkdo.VirtualFileSystem.Abstractions 3.0.0-preview.1

This is a prerelease version of Sparkdo.VirtualFileSystem.Abstractions.
dotnet add package Sparkdo.VirtualFileSystem.Abstractions --version 3.0.0-preview.1
                    
NuGet\Install-Package Sparkdo.VirtualFileSystem.Abstractions -Version 3.0.0-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sparkdo.VirtualFileSystem.Abstractions" Version="3.0.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sparkdo.VirtualFileSystem.Abstractions" Version="3.0.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Sparkdo.VirtualFileSystem.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sparkdo.VirtualFileSystem.Abstractions --version 3.0.0-preview.1
                    
#r "nuget: Sparkdo.VirtualFileSystem.Abstractions, 3.0.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sparkdo.VirtualFileSystem.Abstractions@3.0.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sparkdo.VirtualFileSystem.Abstractions&version=3.0.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Sparkdo.VirtualFileSystem.Abstractions&version=3.0.0-preview.1&prerelease
                    
Install as a Cake Tool

Sparkdo.VirtualFileSystem.Abstractions

虚拟文件系统产品域的稳定公共契约:逻辑路径、文件来源联合、内容摘要见证、暴露模式、文件声明、校验问题,以及两个出口接口(目录查询 IVirtualFileCatalog 与提供器标记 IVirtualFileProvider)。

本包不读磁盘、不读程序集资源、不校验、不物化、不抛任何异常。它只回答两件事:一条文件声明长什么样,以及怎么问出一张已准备好的目录。

三条硬承诺(都在本地 feed 探针上实测过):

  • 零外部 NuGet 依赖:nuspec 的 net11.0 依赖组是空的。编译与运行只需要 net11.0 共享框架(Microsoft.Extensions.FileProviders.Abstractions 与 Microsoft.Extensions.Primitives 已在其中)。实测本包程序集的引用集合就是 System.Runtime, Microsoft.Extensions.FileProviders.Abstractions, System.Collections。
  • 类型不校验,但语义有边界:new LogicalPath("../bad//.\\x") 照样构造成功。规范校验、路径裁决与物化全在核心包 Sparkdo.VirtualFileSystem 的 Prepare 阶段执行并失败关闭。把契约类型当"数据形状",不要当"验证器"。
  • 声明是值语义:七个类型里六个是 record(LogicalPath 是 readonly record struct),可比较、可 with、可安全跨线程传递;Declarations 里的元素与 TryGetDeclaration 返回的是同一个对象实例,不存在两份事实。

目录

什么时候只引这个包

你的位置 该引什么 为什么
领域库/共享库只想声明"我有哪些文件、逻辑路径是什么、期望摘要是什么",由宿主去装配 本包 不引入编排、不引入 DI 语义,net11.0 组保持空依赖(net10.0 组只声明平台内置的 FileProviders.Abstractions 与 Primitives)
要为虚拟文件系统写另一种目录来源(自定义 IVirtualFileCatalog,例如从数据库或远程清单装载声明) 本包(+ 核心包做物化) 契约层就是给这种实现留的
只想把 IFileProvider 交出去、或从 IVirtualFileProvider 解析 本包 IVirtualFileProvider : IFileProvider 在这里
要真的跑起来:注册文件集、校验、物化、读内容 Sparkdo.VirtualFileSystem 本包只有形状,没有行为
只要标准生态接口,不想引任何 Sparkdo 包 Microsoft.Extensions.FileProviders.Abstractions 本包的存在理由就是给 IFileProvider 加一层"可声明、可见证"的元数据

安装与宿主侧依赖面

dotnet add package Sparkdo.VirtualFileSystem.Abstractions

包自身依赖面(实测 nuspec):<dependencies> 只有一个 targetFramework="net11.0" 的空组。宿主侧另需:

你想做的事 额外要引的包 依据(实测)
构造声明、实现 IVirtualFileCatalog、传 IVirtualFileProvider 无 IFileProvider 与 IChangeToken 由 net11.0 共享框架提供
自己 new ServiceCollection() 并 BuildServiceProvider() Microsoft.Extensions.DependencyInjection 容器实现不在共享框架内(Abstractions 在)
用 PhysicalFileProvider/CompositeFileProvider 组合 Microsoft.Extensions.FileProviders.Physical / .Composite 这些实现也不在共享框架内

只引本包、不引任何 Microsoft.Extensions.* 包,是实测过的编译配置——一个 net11.0 控制台工程只 PackageReference 本包即可编译并运行本包的全部类型。

七个类型与它们的角色

类型 形状 谁生产 谁消费 一句话
LogicalPath readonly record struct 核心包(由注册面或枚举派生) 目录查询的主键 NFC、/ 分隔、无根、无空段与 ./.. 段的相对路径
VirtualFileSource abstract record 核心包 物化分发 来源基类;本版本有嵌入式与物理两个封闭子型
EmbeddedFileSource sealed record 核心包 读资源 (AssemblyIdentity, ResourceName)
PhysicalFileSource sealed record 核心包 读盘 (RootPath, RelativePath)
ContentDigest / ContentDigestAlgorithm sealed record / enum 调用方(显式见证)或核心包(现算) 完整性判定 算法 + 64 位小写十六进制 SHA-256
VirtualFileExposure enum 调用方选择、核心包校验 读路径策略 Snapshot(固定内容视图)与 Live(实时读取)
VirtualFileDeclaration sealed record 核心包 IVirtualFileCatalog 一条声明 = 路径 + 来源 + 可选见证 + 暴露
VirtualFileSystemIssue sealed record 核心包 诊断与日志 Code + 中文 Message + 可空 Subject
IVirtualFileCatalog interface 核心包实现(DefaultVirtualFileCatalog) 需要元数据的调用方 按路径查声明、枚举全量声明
IVirtualFileProvider interface : IFileProvider 核心包实现 宿主组合 零成员的解析标记

逐成员语义

LogicalPath

public readonly record struct LogicalPath(string Value);
  • Value 是原样字符串:构造不校验、不重写、不 trim。default(LogicalPath).Value 是 null(结构体默认值不跑构造函数),所以别把 default 当"空路径"用。
  • 相等按 Value 序数比较(record 语义),GetHashCode 一致,可直接进 HashSet<LogicalPath>。
  • ToString() 形态:LogicalPath { Value = Templates/welcome.html }。

VirtualFileSource 与两个子型

public abstract record VirtualFileSource;
public sealed record EmbeddedFileSource(string AssemblyIdentity, string ResourceName) : VirtualFileSource;
public sealed record PhysicalFileSource(string RootPath, string RelativePath) : VirtualFileSource;
  • EmbeddedFileSource.AssemblyIdentity 是程序集的 Assembly.FullName(核心包用它把声明绑回已注册的 Assembly 实例);ResourceName 是清单资源名(GetManifestResourceNames() 返回的那种点分名字),不是逻辑路径。
  • PhysicalFileSource.RelativePath 用 / 分隔(核心包在枚举时已把 \ 换成 /);RootPath 是登记的绝对根路径。
  • 三个类型都是 record,值相等,可 with。模式匹配是核心包分发物化的方式,也是你消费来源信息的唯一姿势:
var kind = declaration.Source switch
{
    EmbeddedFileSource embedded => $"资源 {embedded.ResourceName} 来自 {embedded.AssemblyIdentity}",
    PhysicalFileSource physical => $"磁盘文件 {physical.RootPath}\{physical.RelativePath}",
    _ => "未知来源",
};

ContentDigest 与 ContentDigestAlgorithm

public sealed record ContentDigest(ContentDigestAlgorithm Algorithm, string Value);
public enum ContentDigestAlgorithm { Sha256 }
  • Value 必须是小写 64 位十六进制;格式与算法合法性由核心包在校验阶段判(不合规是 VFS1005),本包不检查——new ContentDigest(ContentDigestAlgorithm.Sha256, "ABC") 能构造出来。
  • 只有 Sha256 一个成员。加新算法会是新增枚举成员(非破坏性),但核心包会拒绝它不认识的值(VFS1005)。

VirtualFileExposure

public enum VirtualFileExposure { Snapshot, Live }   // Snapshot = 0,Live = 1
  • default 是 Snapshot,所以 new VirtualFileDeclaration(path, source, digest, default) 就是快照。
  • Snapshot:Prepare 阶段把内容读进不可变缓冲,之后与磁盘无关。
  • Live:Prepare 只做一次探测与见证记录,每次打开实时读盘。嵌入式来源只能 Snapshot(核心包会拒绝其它组合,VFS1010)。

VirtualFileDeclaration

public sealed record VirtualFileDeclaration(
    LogicalPath LogicalPath,
    VirtualFileSource Source,
    ContentDigest? ContentDigest,
    VirtualFileExposure Exposure);
  • 逻辑路径是 Prepared 目录的主键;Source 决定去哪里取内容;ContentDigest 可空(空=便捷注册,由 Prepare 现算补齐;非空=显式见证,必须与实际内容一致,否则 VFS2002)。
  • 这是"一条声明"的唯一表示:核心包的两个查询出口(TryGetDeclaration 与 Declarations)返回同一个实例,摘要要么两边都有、要么都没有,不会分叉。

VirtualFileSystemIssue

public sealed record VirtualFileSystemIssue(string Code, string Message, string? Subject);
  • Code 是 VFS 前缀的稳定错误码;Message 是中文说明;Subject 通常是出问题的逻辑路径或来源标识,可为 null(如集合级超限)。
  • 本包不抛异常,也不判定问题;它只是核心包把失败原因交出来的载体(消费入口是核心包的 VirtualFileSystemPreparationException.Issues)。

IVirtualFileCatalog

public interface IVirtualFileCatalog
{
    bool TryGetDeclaration(string logicalPath, out VirtualFileDeclaration declaration);
    IReadOnlyList<VirtualFileDeclaration> Declarations { get; }
}
  • TryGetDeclaration:命中返回 true 并给出声明;未命中时 declaration 被赋 null(签名不可空,未命中请立刻丢弃它)。入参非法(null、空白、//、反斜杠、盘符、./.. 段)按"未命中"处理还是抛异常,取决于实现:核心包的实现选择"未命中",只对 null/空白抛 ArgumentNullException/ArgumentException(参数名 logicalPath)。
  • 入参路径允许带恰好一个前导 /(生态调用方的常见形状),返回的声明仍携带规范化后的逻辑路径。
  • Declarations:全量声明,按逻辑路径序数排序;是快照式的只读集合,可以安全枚举(本版本实现返回 ImmutableArray,枚举本身会走 IReadOnlyList 接口,因此每次 foreach 约 56 B——只在需要遍历/序列化时用它,单次查找请走 TryGetDeclaration)。

IVirtualFileProvider

public interface IVirtualFileProvider : IFileProvider;
  • 零自有成员(实测 GetMembers().Length == 0),只作为"这是本产品提供的 IFileProvider"的解析标记存在。
  • 为什么需要它:核心包对标准 IFileProvider 用 TryAdd 注册,宿主若已自行注册(例如 wwwroot 的 PhysicalFileProvider),标准接口就让位;此时可以从 IVirtualFileProvider 拿到本产品的实现,再自己组合。

值语义与默认值实测

var p1 = new LogicalPath("Templates/welcome.html");
var p2 = new LogicalPath("Templates/welcome.html");
p1 == p2;                                   // True
p1.GetHashCode() == p2.GetHashCode();       // True
default(LogicalPath).Value is null;         // True

var source = new EmbeddedFileSource("MyAssembly, Version=1.0.0.0", "MyAssembly.Data.welcome.html");
source == new EmbeddedFileSource("MyAssembly, Version=1.0.0.0", "MyAssembly.Data.welcome.html");   // True

var declaration = new VirtualFileDeclaration(p1, source, null, VirtualFileExposure.Snapshot);
declaration.ContentDigest is null;                          // True(便捷注册形状)
declaration with { Exposure = VirtualFileExposure.Live };   // 只改暴露,其余保留

record 的 ToString() 会打印全部成员,适合直接进日志:

EmbeddedFileSource { AssemblyIdentity = MyAssembly, Version=1.0.0.0, ResourceName = MyAssembly.Data.welcome.html }
ContentDigest { Algorithm = Sha256, Value = aaaa…(64 个 a) }
VirtualFileSystemIssue { Code = VFS1003, Message = 路径不规范。, Subject = a/../b }

逻辑路径的字符规则(谁来判、判多重)

声明侧与查找侧是两套不同严格度的规则,本包只定义形状,规则在核心包:

面向 规则 违反后果
声明侧(进目录的路径) NFC、/ 分隔、无根、无盘符、无 \、无空段、无 ./..、段内无控制字符、段不是 Windows 保留名(CON/COM1/CLOCK$ 等)、段不以点或空格结尾、不含 < > : " | ? * Prepare 失败关闭 VFS1003(物理相对路径同理,VFS1008/VFS3005 为防御分支)
查找侧(调用方传进来的 subpath) 只额外容忍恰好一个前导 /(目录形式可再带一个尾随 /);其余非法形状一律按"不存在"处理 不抛异常,返回 Exists=false 的占位对象 / Exists=false 的列举

以点开头的名字(.gitignore、.env、.editorconfig)是合法逻辑路径——Windows 与 POSIX 都允许这类名字;核心包早期版本把"点之前的基名为空"误判成保留名,会让一个 dotfile 干掉整次 Prepare,现已修正并有用例钉住。同时"整段都是点"(.、..、...)仍被拒绝,因为 Windows 会把它折叠成当前/上级目录语义。

摘要见证的分级

注册方式 见证从哪来 不符时
便捷注册(AddEmbedded<TMarker>()、AddPhysical(root) 枚举) Prepare 现算 SHA-256 并写进声明,Declarations 与 TryGetDeclaration 都能读到 不存在"不符",因为值就是当场算的
显式声明并给出 ContentDigest 调用方提供 Prepare 失败关闭 VFS2002(逐字消息:文件内容与声明的 SHA-256 摘要不一致。)
显式声明但不给见证 Prepare 现算补齐 同上,不报错

Live 暴露的文件也遵循这套:Prepare 读一次算见证,之后的读不再校验——磁盘内容变了,声明里的摘要仍是 Prepare 时那一份。

来源联合与扩展缝

VirtualFileSource 是 abstract record,本版本只有两个封闭子型。新增来源(例如未来的生成文件、内存来源)走"加子型 + 核心包加物化分支",公共接缝不变。

需要知道的边界:

  • 核心包对不认识的来源子型直接失败关闭(VFS1013),不会静默跳过。
  • EmbeddedFileSource 的 identity 必须与已注册程序集一致;同一 identity 绑到不同 Assembly 实例会被拒(VFS4005),程序集没有 FullName(匿名/dynamic)也无法登记(VFS4002)。
  • 只有核心包能真正"打开"来源——本包的来源记录是纯数据,不含句柄、不含流。

两个出口接口:谁实现、谁消费

你的应用
  ├─ 读内容:Microsoft.Extensions.FileProviders.IFileProvider      ← 主读口(由核心包注册)
  ├─ 读元数据:Sparkdo.VirtualFileSystem.IVirtualFileCatalog        ← 声明/摘要/暴露查询
  └─ 解析本产品实现:IVirtualFileProvider(标准接口被宿主占用时的回退口)

核心包 Sparkdo.VirtualFileSystem
  ├─ 实现 IVirtualFileProvider(内部类型 VirtualFileProvider)
  └─ 实现 IVirtualFileCatalog(内部类型 DefaultVirtualFileCatalog)

主读口是 IFileProvider:GetFileInfo / GetDirectoryContents / Watch 三个成员足以读完内容和列举。IVirtualFileCatalog 只在你要问"这条路径是从哪来的、内容摘要是什么、是快照还是实时"时才用。

自己实现一个只读目录

契约层的全部消费面都不需要 DI。下面是一个可直接编译的自定义目录(实测形状):

using Sparkdo.VirtualFileSystem;

public sealed class MemoryVirtualFileCatalog(
    IReadOnlyList<VirtualFileDeclaration> declarations) : IVirtualFileCatalog
{
    public bool TryGetDeclaration(string logicalPath, out VirtualFileDeclaration declaration)
    {
        // 签名声明不可空,未命中必须显式赋 null!。
        declaration = declarations.FirstOrDefault(d =>
            string.Equals(d.LogicalPath.Value, logicalPath, StringComparison.Ordinal))!;
        return declaration is not null;
    }

    public IReadOnlyList<VirtualFileDeclaration> Declarations { get; } = declarations;
}

var catalog = new MemoryVirtualFileCatalog(
[
    new VirtualFileDeclaration(
        new LogicalPath("Templates/email/welcome.html"),
        new EmbeddedFileSource("MyApp, Version=1.0.0.0", "MyApp.Templates.email.welcome.html"),
        ContentDigest: null,
        VirtualFileExposure.Snapshot),
    new VirtualFileDeclaration(
        new LogicalPath("data/region.json"),
        new PhysicalFileSource(@"C:\app\files", "data/region.json"),
        new ContentDigest(ContentDigestAlgorithm.Sha256, "0".PadRight(64, '0')),
        VirtualFileExposure.Live),
]);

catalog.Declarations.Count;                                    // 2
catalog.TryGetDeclaration("data/region.json", out var hit);    // True
catalog.TryGetDeclaration("nope", out var miss);               // False,miss 为 null

要点:主键比较请用 StringComparison.Ordinal 并与本域其余实现保持一致(区分大小写);Declarations 建议按路径序数排序,方便 diff 与日志对齐。

实现者守则

  1. 不要在契约类型里塞校验:形状稳定是本包的全部职责,判定属于核心包。
  2. TryGetDeclaration 未命中时只赋 null,不要抛;不要返回"空的声明对象"当未命中。
  3. Declarations 必须是只读快照语义:不要在枚举中途改内部列表让调用方看到撕裂结果。
  4. Declarations 的元素与 TryGetDeclaration 的返回值应是同一实例(或值相等的同一份事实)——不要让两个出口给出不同的摘要。
  5. Declarations 排序请固定(推荐逻辑路径序数升序),否则日志与快照测试会漂。
  6. 不要新增 VirtualFileSource 的子型却指望核心包认识它——核心包对未知来源失败关闭(VFS1013)。
  7. 不要把 EmbeddedFileSource.ResourceName 写成逻辑路径:它是点分的清单资源名。
  8. PhysicalFileSource.RelativePath 用 /,不要留 \(核心包会按非规范路径拒绝)。
  9. ContentDigest.Value 给小写 64 位十六进制;大写或短串会被 VFS1005 拒绝。
  10. 别给嵌入式来源设 Live:核心包会 VFS1010 失败关闭。
  11. 别把 default(LogicalPath) 当空路径传下去:它的 Value 是 null。
  12. 自定义目录若要与核心包互换使用,请同样容忍"恰好一个前导 /"的入参形状,否则静态文件中间件那类调用方会全量未命中。

异常一览

本包不主动抛出任何异常:七个类型都是数据形状,两个接口没有默认实现。而且契约类型对 null 一律不设防(实测):

new LogicalPath(null!);                                    // 不抛;ToString() 打印 LogicalPath { Value =  }
default(LogicalPath).Value is null;                        // True
default(LogicalPath).GetHashCode();                        // 0(不抛)
default(LogicalPath) == default(LogicalPath);              // True
new EmbeddedFileSource(null!, null!);                       // 不抛
new VirtualFileDeclaration(default, null!, null, default); // 不抛,Source 为空、Exposure 为 Snapshot
new VirtualFileSystemIssue(null!, null!, null!);            // 不抛
场景 结果(实测)
用 null/空串构造上述任何记录 构造成功、可打印;不抛异常。防线在核心包:路径非法 → VFS1003,来源缺字段 → VFS1009/VFS1013,摘要格式非法 → VFS1005
把 default(LogicalPath) 当"空路径"传下去 能传,但它的 Value 是 null;核心包按非规范路径处理(VFS1003),别指望它等于"根"
自定义 IVirtualFileCatalog 对 null 入参 由你自己决定。核心包实现抛 ArgumentNullException(logicalPath)(传 null)与 ArgumentException(传空白串)
核心包 Prepare 失败 Sparkdo.VirtualFileSystem.VirtualFileSystemPreparationException(类型在核心包) 携带 IReadOnlyList<VirtualFileSystemIssue>,逐条 Code/Message/Subject

非目标

  • 不做路径规范化、不做 NFC 重写、不做大小写折叠——本包不解释字符串含义。
  • 不做摘要计算与比对(SHA256 只在核心包的物化里算)。
  • 不做文件读取、不定义读取管道、不暴露任何 Stream。
  • 不做 DI 扩展、不做配置对象、不做 Options 模式;接线全在核心包。
  • 不做文件版本、时间戳、权限、扩展属性等元数据——声明模型刻意只保留"路径 + 来源 + 见证 + 暴露"。
  • 不定义自己的门面接口;IFileProvider 就是读口。
  • 不提供 GeneratedFileSource 等未来来源(抽象基类只是留缝)。
  • 不做变更通知(Watch 属于 IFileProvider,其令牌语义由核心包给出)。
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Sparkdo.VirtualFileSystem.Abstractions:

Package Downloads
Sparkdo.VirtualFileSystem

Sparkdo 虚拟文件系统:声明式文件集校验与物化、失败关闭的物理句柄级验证、不可变快照与标准 IFileProvider 适配,AddSparkdoVirtualFileSystem() 一行接线。

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0-preview.1 62 10/30/2026
2.0.0-preview.2 102 7/20/2026
2.0.0-preview.1 92 7/18/2026
1.0.4-preview.3 502 6/8/2026
1.0.4-preview.2 487 6/7/2026
1.0.4-preview.1 460 6/7/2026
1.0.3 1,144 6/6/2026
1.0.3-preview.1 479 5/17/2026
1.0.2 857 4/27/2026
1.0.2-preview.5 192 4/15/2026
1.0.2-preview.4 200 2/8/2026
1.0.2-preview.3 201 2/1/2026
1.0.2-preview.2 229 1/31/2026
1.0.2-preview.1 240 12/4/2025
1.0.1 2,620 11/27/2025
1.0.0 2,504 11/25/2025
1.0.0-preview.5 184 10/24/2025