关于 model-building
model-building 是 Entity Framework Core 约定、元数据、初始化和关系模型的开发参考技能,涵盖端到端变更、兼容性问题及针对性测试验证。
主要功能
- 01约定系统与元数据层级
- 02可配置元数据属性的实现
- 03模型生命周期与初始化
- 04运行时及编译模型传递
- 05快照兼容性指引
- 06模型验证与测试位置
适用场景
- 01添加可配置的元数据属性
- 02调试约定与关系发现
- 03将注释传递到运行时模型
- 04避免模型变更后产生意外迁移
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ efcore --skill model-building安装后在 Claude Code 中输入/model-building确认已加载。
详细介绍
模型构建、约定与初始化
涵盖模型构建(约定、流畅 API、元数据层级)和模型初始化(运行时注解传播、编译模型筛选)。
约定系统
ConventionSet(src/EFCore/Metadata/Conventions/ConventionSet.cs)为每种元数据事件保存一个 List<I*Convention>。src/EFCore/Metadata/Conventions/ 中的重要约定包括:
DbSetFindingConvention— 从DbSet<T>中发现实体PropertyDiscoveryConvention— 从 CLR 类型中发现属性KeyDiscoveryConvention— 查找主键(Id、TypeId)RelationshipDiscoveryConvention— 根据导航推断外键RuntimeModelConvention— 从可变模型创建优化后的RuntimeModel
重写 ConfigureConventions(ModelConfigurationBuilder) 以添加或移除约定。
修复约定时,必须保留分派器行为和歧义信息。对于反向导航特性,不要通过永久忽略有歧义的导航来解决递归问题:移除一个候选项可能会让另一个歧义变得唯一,并配置错误的关系。InversePropertyAttributeConvention 会记录反向候选项,并使用 AddInverseNavigation、TryRemoveIfAmbiguous 和 FindAmbiguousInverse,同时继续进行约定处理。
测试状态转换,而不只是初始的有歧义模型:首先验证多个相互作用的候选项不会配置任何关系;然后分别通过忽略/移除一个竞争导航,以及移除一个竞争实体类型,真正消除歧义,并验证剩余的反向关系已配置。这些情况可以区分候选项跟踪与永久忽略的变通方案。
元数据接口层级
IReadOnly* → IMutable* → IConvention* → IRuntime*
适用于:Model、EntityType、Property、Key、ForeignKey、Navigation、Index 等。Builder 的层级为:*Builder → IConvention*Builder。
添加可配置的元数据属性
从头到尾参照最接近的现有属性实现,不要只修改公共接口:
- 添加只读、可变和约定元数据契约、具体存储、默认值、配置源、约定 setter 和 Builder 优先级检查。替换元数据或重建关系时,保留该属性。
- 如果约定可以观察到变更,则添加约定接口、
ConventionSet中所有添加/替换/移除路径,以及即时和延迟的分派器作用域与节点。 - 通过
RuntimeModelConvention、运行时构造函数/Builder 和CSharpRuntimeModelCodeGenerator将值传递到优化后的运行时元数据中,以确保编译模型保留该值。 - 如果快照会持久化该属性,则更新流畅 Builder API 和
CSharpSnapshotGenerator;旧快照中缺少该属性时,应等同于历史默认值。 - 更新公共 API 基线,并覆盖配置源优先级、约定分派、运行时/编译模型传播和快照往返测试。
模型生命周期
- 可变模型 — 在
OnModelCreating期间由ModelBuilder构建,通过FinalizeModel()设为只读 - 设计时模型 — 已完成且只读的
Model,还包含迁移使用的仅设计时注解 - 运行时模型 — 由
RuntimeModelConvention.ProcessModelFinalized()创建的优化只读模型,不包含仅设计时注解
ModelRuntimeInitializer.Initialize()(由 DbContextServices.CreateModel() 调用):
Initialize(model, designTime, validationLogger)
├─ FinalizeModel() if mutable
├─ Set ModelDependencies, InitializeModel
└─ RuntimeModelConvention creates RuntimeModel, copies/filters annotations
复杂类型属性递归
在约定或验证中处理属性时,请记住复杂类型可以包含其自身声明的属性。使用 GetFlattenedProperties() 遍历所有属性(包括嵌套的非集合复杂类型上的属性),或手动递归遍历 GetDeclaredComplexProperties() → complexProperty.ComplexType。
添加新注解
- 在
CoreAnnotationNames及其AllNames中添加常量 - 如果这是仅用于设计时的注解(仅用于迁移操作),则在
RuntimeModelConvention.ProcessModelAnnotations中进行筛选- 如果该注解被筛除,则在 getter 中添加逻辑:如果在运行时模型上访问该注解,则抛出异常
- 如果该注解可在运行时延迟计算(例如,基于其他注解),则在
CSharpRuntimeAnnotationCodeGenerator.Generate中进行筛选 - 如果该注解用于升级迁移或关系模型,则在
RelationalAnnotationProvider中传递;如果用于降级迁移,则在IMigrationsAnnotationProvider中传递
关系模型
RelationalModel(src/EFCore.Relational/Metadata/Internal/RelationalModel.cs)是以数据库为中心的 EF 模型视图,将实体类型映射到物理数据库对象:Tables、Views、Functions、Queries 和 DefaultTables。DefaultTables 是仅用于 FromSql 查询的伪表对象。
由 RelationalModelRuntimeInitializer 延迟创建,通过 model.GetRelationalModel() 访问。用于迁移(MigrationsModelDiffer)、更新和查询管道。
RelationalAnnotationProvider 会填充关系模型元素上的注解。提供程序子类(例如 SqlServerAnnotationProvider)会添加提供程序特定的注解。IMigrationsAnnotationProvider 控制降级迁移操作中使用的注解。
模型验证
ModelValidator(src/EFCore/Infrastructure/ModelValidator.cs)和 RelationalModelValidator(src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs)会在模型最终确定后运行,即 ModelRuntimeInitializer.Initialize() 中的前置和后置验证 InitializeModel 调用之间。
迁移快照兼容性
模型构建变更可能会导致升级的用户遇到虚假迁移。原因有两个:
- 快照中写入了新的元数据 — 旧快照中没有该元数据;
MigrationsModelDiffer会检测到差异。修复方法:确保将旧快照中缺少该注解的情况视为旧默认值。 - 注解已重命名或重新解释 — 旧快照会生成不同的模型。修复方法:保留向后兼容的读取逻辑。
检查 CSharpSnapshotGenerator(写入的内容)和 MigrationsModelDiffer(如何处理缺失情况)。在 test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs 中添加快照往返测试。
测试
| 区域 | 位置 |
|---|---|
| 约定单元测试 | test/EFCore.Tests/Metadata/Conventions/ |
| 元数据单元测试 | test/EFCore.Tests/Metadata/Internal/ |
| 模型构建器 API 测试 | test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest*.cs |
| 关系发现测试 | test/EFCore.Specification.Tests/ModelBuilding101*.cs |
| 模型验证测试 | test/EFCore.Tests/Infrastructure/ModelValidatorTest*.cs |
| 已编译模型测试 | test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs |
验证
- 模型构建期间,在最终确定时不会出现
InvalidOperationException - 所有新增 API 均有测试覆盖
- 使用
EF_TEST_REWRITE_BASELINES=1可顺利更新已编译模型基线 - 元数据对象上的
ToString()能简洁地显示内容,且不会抛出异常 - 针对已有快照的项目不会生成虚假迁移