EF Core SQL Server Abstractions 包详解:独立于 EF Core 的 HierarchyId 层次数据结构
【免费下载链接】efcoreEF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.项目地址: https://gitcode.com/GitHub_Trending/ef/efcore
Microsoft.EntityFrameworkCore.SqlServer.Abstractions是 EF Core SQL Server 提供商体系中的一个轻量抽象包,它只暴露极少数核心类型(目前即HierarchyId),专为那些不希望引入完整Microsoft.EntityFrameworkCore.SqlServer依赖、却需要在 POCO 实体或应用服务层表达 SQL Server 层次数据(hierarchyid)的应用而设计。读完本文,你将了解该包的定位与安装时机、HierarchyId类型的完整 API 能力与底层实现细节,以及它与 EF Core 主包、Microsoft.EntityFrameworkCore.SqlServer.HierarchyId提供程序包之间的协作关系。
包的定位:何时只需要 Abstractions 包
根据 src/EFCore.SqlServer.Abstractions/README.md 的说明,该包是一个“包含若干抽象(abstractions)的小包,这些抽象对于在不想依赖完整Microsoft.EntityFrameworkCore.SqlServer的场景中的应用可能有用”。原文给出的典型场景是:在 POCO 实体类型中使用HierarchyId,而这些实体类型本身与 EF Core 无关——例如一个纯领域模型库,只需要表达树形结构中的位置(深度与广度),却不想让整个库背上完整的 EF Core SQL Server 提供商依赖。
该包与主包的关系遵循“自动附带、显式安装”的原则:
- 自动附带:README 指出该包会作为主包
Microsoft.EntityFrameworkCore.SqlServer的依赖自动引入。因此,如果你已经引用了完整的 SQL Server 提供商,通常无需额外安装。 - 显式安装:只有在刻意避免使用主包的场合(如上文的纯 POCO 场景、跨层共享的领域模型库)才单独安装 Abstractions 包。
包内实际内容:一个公共类型与两个依赖
从源码结构看,该包的公共 API 面非常收敛。EFCore.SqlServer.Abstractions.baseline.json 是 API 一致性基线文件,其中登记的全部公共类型只有Microsoft.EntityFrameworkCore.HierarchyId(实现IComparable<HierarchyId>),与 EFCore.SqlServer.Abstractions.csproj 中Description项的声明一致:
<Description>Provides abstractions that are used by models in conjunction with the SQL Server EF Core provider Commonly Used Types: Microsoft.EntityFrameworkCore.HierarchyId </Description>csproj 中还揭示了该包仅有的两个 NuGet 依赖(EFCore.SqlServer.Abstractions.csproj):
Microsoft.SqlServer.Types:提供底层的SqlHierarchyId类型,HierarchyId的所有计算逻辑都委托给它;System.Text.Json:用于支持内置的 JSON 序列化转换器。
HierarchyId 类型 API 详解
HierarchyId定义于 src/EFCore.SqlServer.Abstractions/HierarchyId.cs,其内部仅持有一个SqlHierarchyId _value字段(L18),是一个对SqlHierarchyId的托管封装。由于主包与 Abstractions 包共用Microsoft.EntityFrameworkCore根命名空间(csproj 中RootNamespace即为此),从代码视角看两者类型是无缝的——这也是该包能作为主包依赖自然融合的原因。
构造与解析
源码(HierarchyId.cs L23-L49)提供三个构造函数:
public HierarchyId() : this(SqlHierarchyId.GetRoot()) { } public HierarchyId(string value) : this(SqlHierarchyId.Parse(value)) { } public HierarchyId(SqlHierarchyId value) { if (value.IsNull) { throw new ArgumentNullException(nameof(value)); } _value = value; }- 无参构造等价于
GetRoot(),即根节点/; - 字符串构造接受节点的正则字符串表示(如
/1/2/3/); - 传入
SqlHierarchyId时若其值为Null会抛出ArgumentNullException——这是该封装与底层类型的一个关键差异点:HierarchyId实例不允许承载 SQL NULL,空值语义由外层可空引用类型HierarchyId?表达。
静态解析方法有两个重载(L55-L77):
public static HierarchyId? Parse(string? input) // input 为 null 时返回 null public static HierarchyId Parse(HierarchyId parentHierarchyId, params int[] parentId)第二重载是SqlHierarchyId本身不具备的能力。从实现 GenerateHierarchyIdBasedOnParent 可以看到,它把父节点字符串与变长路径段用点号拼接后再解析:
var specificPath = new StringBuilder(parent.ToString()); specificPath.Append(string.Join(".", parentId)); specificPath.Append('/'); return Parse(specificPath.ToString());因此HierarchyId.Parse("/1/", 2, 3)会得到/1/2.3/(多段路径),HierarchyId.Parse("/1/2/", 3)得到/1/2/3/(单段路径)。源码注释也明确指出了这一点:“It can be more than one element if want have path like: /1/2/3.1/, otherwise one element for have path like: /1/2/3/.”
树形操作的典型用法
以下示例演示常见树操作,均直接对应源码中的虚拟方法(virtual修饰意味着在内存提供程序等场景下可以派生替换实现):
var root = HierarchyId.GetRoot(); // "/" var dept = HierarchyId.Parse("/1/"); var emp = HierarchyId.Parse("/1/2/"); emp.GetLevel(); // 2:根节点为第 0 层 emp.GetAncestor(1); // /1/:上溯 1 层 dept.GetDescendant(); // 在 /1/ 下生成一个新子节点 dept.GetDescendant(child1, child2) // 在两个已有子节点之间插入新节点 emp.IsDescendantOf(dept); // true:判断是否位于某子树内 emp.GetReparentedValue(dept, "/3/");// 将节点移动到新的父节点下关键语义(均见 HierarchyId.cs 的 XML 文档注释):
GetAncestor(n):返回上溯 n 层的祖先;若 n 超过GetLevel()则返回null;n 为负数抛出ArgumentOutOfRangeException。GetDescendant(child)/GetDescendant(child1, child2):分别用于追加新子节点(传入“最后一个已有子节点”以确保不冲突)和在两个已有子节点之间插入新节点。GetReparentedValue(oldRoot, newRoot):把以oldRoot为终点的路径段重挂到newRoot之下,等价于树的“移动”操作。
比较运算符与类型转换
HierarchyId重载了全部六个比较运算符(L181-L227),实现方式统一为委托给底层SqlHierarchyId.CompareTo:
public static bool operator ==(HierarchyId? hid1, HierarchyId? hid2) => ((SqlHierarchyId)hid1).CompareTo(hid2) == 0;此外提供两个显式方向不同的转换运算符(L234-L243):
public static implicit operator SqlHierarchyId(HierarchyId? value) => value?._value ?? SqlHierarchyId.Null; // null 转回 SqlHierarchyId.Null public static explicit operator HierarchyId?(SqlHierarchyId value) => value.IsNull ? null : new HierarchyId(value);即:HierarchyId?可以隐式降级为SqlHierarchyId(null 映射为SqlHierarchyId.Null),反向转换是显式的(SqlHierarchyId.Null映射回null)。这组不对称设计与构造函数拒绝 NULL 值的行为相互呼应。
内置 JSON 序列化:canonical 字符串
HierarchyId通过特性声明了自己的 JSON 转换器(HierarchyId.cs L15):
[JsonConverter(typeof(HierarchyIdJsonConverter))] public class HierarchyId : IComparable<HierarchyId>转换器实现见 src/EFCore.SqlServer.Abstractions/Internal/HierarchyIdJsonConverter.cs:序列化时写出节点的正则字符串表示,反序列化时用HierarchyId.Parse还原,null 值直接透传:
public override HierarchyId? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetString() is null ? null : HierarchyId.Parse(value); public override void Write(Utf8JsonWriter writer, HierarchyId? value, JsonSerializerOptions options) => writer.WriteStringValue(value?.ToString());这意味着System.Text.Json序列化含HierarchyId属性的 POCO 时会得到形如"/1/2/"的 JSON 字符串,而不需要调用方再注册自定义转换器——对 ASP.NET Core 控制器、缓存层或跨服务 DTO 场景是一个实用的细节。
与主包和 HierarchyId 提供程序包的协作
理解 Abstractions 包在整个 EF Core SQL Server 体系中的位置,需要看到两个方向的关系:
1. 主包Microsoft.EntityFrameworkCore.SqlServer自动携带它。README 将其描述为主包依赖的一部分,因此常规使用 EF Core SQL Server 提供商的应用无需单独引入。
2. 专项功能包显式引用它。Microsoft.EntityFrameworkCore.SqlServer.HierarchyId提供程序包(EFCore.SqlServer.HierarchyId.csproj)以带包版本约束的项目引用方式依赖 Abstractions 包:
<ProjectReference Include="..\EFCore.SqlServer.Abstractions\EFCore.SqlServer.Abstractions.csproj" PackageVersion="[$(Version), $(NextMajorVersion))" />该提供程序包负责把HierarchyId接入 EF Core 查询管道:类型映射(SqlServerHierarchyIdTypeMapping.cs)、LINQ 方法翻译(SqlServerHierarchyIdMethodTranslator.cs)等。它内部使用ValueConverter<HierarchyId?, SqlHierarchyId>完成模型类型与存储类型的互转,见 SqlServerHierarchyIdValueConverter.cs:
private static SqlHierarchyId ToProvider(HierarchyId? value) => (SqlHierarchyId)value; // 隐式转换 private static HierarchyId? FromProvider(SqlHierarchyId value) => (HierarchyId?)value; // 显式转换这正是前文那组转换运算符在框架内部的落地位置。若要启用完整功能,按 src/EFCore.SqlServer.HierarchyId/README.md 的说明,在配置DbContext时调用HierarchyId()选项即可:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer( "Server=localhost;Database=MyDatabase;Trusted_Connection=True;", b => b.HierarchyId());正确性验证:基线测试与单元测试
仓库中的测试代码印证了该包 API 的完整性:
- test/EFCore.SqlServer.HierarchyId.Tests/SqlServerAbstractionsApiConsistencyTest.cs 基于 API 一致性基线框架,对照 baseline.json 校验公共 API 与基线一致,防止公共成员被意外增删;
- test/EFCore.SqlServer.HierarchyId.Tests/WrapperTests.cs 针对
HierarchyId作为SqlHierarchyId封装层的行为做回归验证; - test/EFCore.SqlServer.HierarchyId.Tests/HierarchyIdJsonConverterTest.cs 覆盖 JSON 序列化/反序列化往返。
值得注意的是测试工程放在EFCore.SqlServer.HierarchyId.Tests下而非独立的 Abstractions 测试工程——从这一组织结构可以推断,Abstractions 包的测试与 HierarchyId 提供程序包的测试合并维护,因为它们共同服务于HierarchyId这一个核心类型。
适用前提与限制小结
| 要点 | 说明 | 依据 |
|---|---|---|
| 目标框架 | 包编译目标为仓库统一的$(NetMinimum)版本(见 csprojTargetFramework) | EFCore.SqlServer.Abstractions.csproj |
| 底层依赖 | 依赖Microsoft.SqlServer.Types提供SqlHierarchyId,该包本身带有平台相关的本机组件,跨平台环境需按其官方说明安装 | EFCore.SqlServer.Abstractions.csproj |
| 空值语义 | HierarchyId实例不可为 SQL NULL,用HierarchyId?可空引用表达空 | HierarchyId.cs L43-L46 |
| 可派生性 | 实例方法均为virtual,可在特殊场景(如内存提供程序)下派生替换 | HierarchyId.cs |
| 使用边界 | 仅含HierarchyId抽象;查询翻译、类型映射等完整功能需另行引入 HierarchyId 提供程序包 | baseline.json |
一句话总结该包的设计取舍:把“层次位置的数学”下沉到一个只依赖Microsoft.SqlServer.Types和System.Text.Json的最小包里,让领域模型可以零成本表达 hierarchyid,而把“如何存入数据库、如何翻译查询”留给主包与专项提供程序包——这正是 EF Core 提供商体系“抽象与实现分离”原则的一个典型样本。
【免费下载链接】efcoreEF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.项目地址: https://gitcode.com/GitHub_Trending/ef/efcore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考