1.EF Core 源码探索-模型构建详解-Metadata

一、Metadata是什么?

开始之前我们先想一个问题,当你写了下面这段代码:

public class Order
{
    public int Id { get; set; }
    public string Status { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
}

然后调用查询方法:

context.Orders.Where(o => o.Status == "Pending").ToList()

这里我们既没有提供原生 SQL 语句,也没有在任何地方定义 SQL 转换逻辑。EF 怎么就能正确执行查询了呢?它怎么知道 Order 类对应 Orders 表?怎么知道 int Id 是主键而不是普通列?怎么知道 Status 属性对应的列名也叫 Status

其实在我们.ToList()之前,框架已经帮我们生成了一整套执行规则,并把这套规则缓存到内存中,它就是 Metadata(模型元数据)

框架会通过以下方式加载规则:

  • EF允许我们直接在属性或类上配置指定的Attribute配置规则,这种代码风格叫DataAnnotations。
  • 也支持通过重写 OnModelCreating 方法,然后使用建造者模式进行流式配置,这种代码风格叫 FluentAPI。
  • 如果以上两种方式都没配置,则由框架自己提供一套兜底规则。

不论哪种方式,这些规则最终都会被解析并存储到元数据中,并以此作为后续执行逻辑的必要依据,所以读懂 Metadata 模块以后,再看其他模块就会清晰很多。

二、Metadata 里面有什么?

Metadata 的核心数据结构是一棵对象树,代码位于 src/EFCore/Metadata/ 目录中:

IModel                      ← 整个数据库的全部描述(根节点)
  └── IEntityType           ← 对应一张表(如 Orders 表)
        ├── IProperty       ← 对应一列(如 Status 列)
        ├── IKey            ← 主键(如 Id)
        ├── IForeignKey     ← 外键关系
        ├── INavigation     ← 导航属性(如 Order.Items)
        └── IIndex          ← 索引

仔细观察该目录下的其他文件,会发现这里提到的每个概念都有四个变体IReadOnly*IMutable*IConvention*I*,把"*"替换成具体的对象就变成以下分组:

# 属性类
IProperty
IReadOnlyProperty
IMutableProperty
IConventionProperty

# 主键类
IKey
IReadOnlyKey
IMutableKey
IConventionKey

# 外键类
IForeignKey
IReadOnlyForeignKey
IMutableForeignKey
IConventionForeignKey

# 导航属性类
INavigation
IReadOnlyNavigation
IMutableNavigation
IConventionNavigation

# 索引类
IIndex
IReadOnlyIndex
IMutableIndex
IConventionIndex

这样设计的原因是:同一个对象在不同阶段需要有不同的访问权限。

  • 运行期必须只读:元数据是进程级缓存,同一个 AppDbContext 类型的所有实例共享同一份,如果运行时还能随意修改,就会出现并发安全问题。所以运行起来以后执行 CRUD 时拿到的都是只读视图(IEntityTypeIProperty)。

  • 约定阶段需要"记录是谁写的":框架约定(Convention)、数据注解(DataAnnotation)、Fluent API 三者都能配置同一个属性,必须有机制来仲裁谁说了算。IConvention* 的每次写入都会附带一个 ConfigurationSource(枚举值 0/1/2),后来的配置只有在来源优先级大于等于之前的值时才允许覆盖,这是"可追溯、可仲裁"的关键。

  • Fluent API 只需要纯粹的写入能力OnModelCreating 里的配置被标记为 Explicit,优先级最高,不需要参与约定系统的仲裁流程,所以用更简单的 IMutable* 即可。

  • Finalize 后切换为运行时专属对象FinalizeModel() 会把可变的构建期元数据转换为 RuntimeEntityTypeRuntimeProperty 等只读对象,同时把属性访问、值比较器预编译成委托,运行期直接读这套对象,不再反射,性能更好。

每个类型都有自己专属的职责,下面逐一说明:

  • IProperty — 列的相关配置

    查询时选哪些列、SaveChanges 时怎么处理这一列的值、变更追踪时怎么比较新旧值——全都读 IProperty

    • IsNullable:这一列允不允许 NULL
    • ValueGenerated:值是自己填(Never)、INSERT 时数据库生成(OnAdd,即自增),还是每次写入都由数据库生成(OnAddOrUpdate
    • IsConcurrencyToken:是否作为乐观并发令牌,SaveChanges 时会把这一列的原始值加进 WHERE 子句做冲突检测
    • GetTypeMapping():这一列的 C# 类型和数据库类型之间的映射关系(比如 string 对应 nvarchar(max)
    • GetMaxLength()IsUnicode()GetPrecision() 等列约束
  • IKey — 主键或唯一键

    IKey 描述一组构成键的列(支持复合键)IEntityType 上有两类键:通过 FindPrimaryKey() 拿到主键,通过 GetKeys() 拿到所有键(包括用 HasAlternateKey 配置的备用唯一键)。IdentityMap(变更追踪的注册表)就是用主键来定位实体的。

    • Properties:构成这个键的 IProperty 列表(复合主键时有多个)
    • GetReferencingForeignKeys():哪些外键引用了这个键(从 IKey 出发可以找到所有指向它的关系)
  • IForeignKey — 模型关系的相关配置

    比如你写 Include(o => o.Customer),EF Core 就是通过 IForeignKey 来知道该 JOIN 哪张表、用哪些列做关联条件的。

    • IForeignKey 是两张表之间关系的元数据,包含关系双方的全部信息:
    • Properties:从表上的外键列
    • PrincipalKey:主表被外键关联的列
    • DeclaringEntityType / PrincipalEntityType:从表和主表各是谁
    • DependentToPrincipal / PrincipalToDependent:关系两侧的导航属性(Order.CustomerCustomer.Orders
    • IsUnique:是否是一对一关系
    • DeleteBehavior:级联删除策略(置 NULL、报错等)
  • IIndex — 数据库索引的描述

    IIndex 记录了要在哪些列上建索引,在 Migration 阶段使用——IMigrationsModelDiffer 对比模型差异时,会检查索引是否需要新增或删除,生成对应的 CREATE INDEX / DROP INDEX 脚本。运行时查询翻译不直接用它,索引的生效与否是数据库自己的事。

    • Properties:构成索引的列(支持复合索引)
    • IsUnique:是否是唯一索引
    • Name:索引名称

三、模型是怎么构建出来的?

明白了 Metadata 是什么之后,下一个问题是:这棵对象树是什么时候、怎么被填充起来的?

入口是 ModelSourcesrc/EFCore/Infrastructure/ModelSource.cs)。当 DbContext 第一次被使用时,它会来这里取模型。

3.1 先取缓存

// Infrastructure/ModelSource.cs
public virtual IModel GetModel(DbContext context, ...)
{
    var cacheKey = Dependencies.ModelCacheKeyFactory.Create(context, designTime);

    if (!cache.TryGetValue(cacheKey, out IModel? model))
    {
        lock (_syncObject)  // 加锁,确保只构建一次
        {
            if (!cache.TryGetValue(cacheKey, out model))
            {
                model = CreateModel(...);  // 真正的构建过程
                cache.Set(cacheKey, model, ...);
            }
        }
    }
    return model!;
}

3.2 缓存里没有,执行构建过程

// Infrastructure/ModelSource.cs
protected virtual IModel CreateModel(DbContext context, ...)
{
    // 第一步(打开冰箱):把约定规则装载进来,建一个空模型
    var modelBuilder = new ModelBuilder(conventionSetBuilder.CreateConventionSet(), ...);

    // 第二步(把大象放进去):调用你重写的 OnModelCreating,把 Fluent API 配置写进模型
    context.OnModelCreating(modelBuilder);

    // 第三步(关门儿):运行收尾约定,冻结模型,转换为运行时格式
    modelBuilder.FinalizeModel();

    return modelBuilder.Model;
}
  • 第一步:加载约定(Convention)

    约定是 EF Core 的自动推断规则,是一种约定大于配置的体现。约定分布在两个项目里:核心约定在 src/EFCore/Metadata/Conventions/,关系型约定在 src/EFCore.Relational/Metadata/Conventions/

    类型 类名 作用
    Core DbSetFindingConvention 扫描 DbContext 上的所有 DbSet<T> 属性,把 T 注册为实体类型
    Core BaseTypeDiscoveryConvention 根据 CLR 继承层次,自动发现基类和派生类的继承关系
    Core NotMappedTypeAttributeConvention 标有 [NotMapped] 的类不注册为实体类型
    Core KeylessAttributeConvention 标有 [Keyless] 的类配置为无主键实体(用于映射视图/查询)
    Core OwnedAttributeConvention 标有 [Owned] 的类自动配置为从属实体类型(Owned Type)
    Core ComplexTypeAttributeConvention 标有 [ComplexType] 的类配置为复杂类型
    Core EntityTypeConfigurationAttributeConvention 标有 [EntityTypeConfiguration<T>] 的类自动应用对应的 IEntityTypeConfiguration<T> 配置
    Core PropertyDiscoveryConvention 扫描实体类的公开标量属性,把可读写的注册为列
    Core ComplexPropertyDiscoveryConvention 扫描复杂类型属性,把已配置为复杂类型的属性注册为复杂属性
    Core ServicePropertyDiscoveryConvention 发现服务属性,如 ILazyLoader(用于懒加载)
    Core NotMappedMemberAttributeConvention 标有 [NotMapped] 的属性/字段忽略不映射
    Core BackingFieldConvention 按命名规则(_namem_namename_ 等)自动发现属性的后备字段
    Core BackingFieldAttributeConvention 标有 [BackingField("_xxx")] 的属性,按指定字段名配置后备字段
    Core NavigationBackingFieldAttributeConvention 导航属性上的 [BackingField],为导航配置后备字段
    Core AutoLoadConvention 根据 Provider 的启发式规则决定属性是否自动加载
    Core ElementMappingConvention 确保基元集合属性的元素类型映射被正确发现
    Core ElementTypeChangedConvention 监听基元集合元素类型变化,同步更新相关配置
    Core KeyDiscoveryConvention 属性名为 Id<类名>Id 时,自动设为主键(忽略大小写)
    Core KeyAttributeConvention 标有 [Key] 的属性设为主键
    Core ForeignKeyIndexConvention 外键属性上自动创建索引(若尚未被主键或现有索引覆盖)
    Core IndexAttributeConvention 实体类上的 [Index] 特性自动创建对应索引
    Core RelationshipDiscoveryConvention 扫描导航属性,只要没有歧义就自动推断一对多、一对一、多对多关系
    Core ForeignKeyPropertyDiscoveryConvention 按命名约定发现外键列:[导航名][主键名][导航名]Id[主表名][主键名][主表名]Id(均忽略大小写)
    Core ForeignKeyAttributeConvention 导航属性上的 [ForeignKey("xxx")] 指定外键列
    Core InversePropertyAttributeConvention [InverseProperty("xxx")] 明确配置反向导航,消除关系歧义
    Core ManyToManyJoinEntityTypeConvention 发现多对多关系后,自动创建中间联接实体类型及其两个外键
    Core CascadeDeleteConvention 必填关系(IsRequired = true)自动设置级联删除;可选关系设为置 NULL
    Core DeleteBehaviorAttributeConvention 导航属性上的 [DeleteBehavior(xxx)] 配置删除行为
    Core RequiredNavigationAttributeConvention 导航属性上的 [Required] 将主体端配置为必填
    Core NavigationEagerLoadingConvention 从属实体(Owned Type)的导航属性自动配置为急加载
    Core NonNullableNavigationConvention 开启 C# 可空引用类型时,非空导航属性自动标记为必填关系
    Core RequiredPropertyAttributeConvention [Required] 属性自动标记为不可为 NULL
    Core MaxLengthAttributeConvention [MaxLength(n)] 自动设置列最大长度
    Core StringLengthAttributeConvention [StringLength(n)] 自动设置列最大长度(与上类似,来自不同命名空间)
    Core PrecisionAttributeConvention [Precision(p, s)] 自动设置数值精度和小数位数
    Core UnicodeAttributeConvention [Unicode(false)] 将 string 属性配置为非 Unicode(如 varchar
    Core ConcurrencyCheckAttributeConvention [ConcurrencyCheck] 将属性配置为乐观并发令牌
    Core TimestampAttributeConvention [Timestamp] 将属性配置为并发令牌且值由数据库在每次写入时生成
    Core DatabaseGeneratedAttributeConvention [DatabaseGenerated(xxx)] 配置值生成策略(None / Identity / Computed)
    Core ValueGenerationConvention 整数/GUID 主键自动配置为数据库值生成(ValueGenerated.OnAdd
    Core ChangeTrackingStrategyConvention 若模型中所有实体都实现了通知接口,自动切换为通知式变更追踪策略,节省快照开销
    Core DiscriminatorConvention 继承层次默认使用 TPH(单表继承),自动添加 Discriminator 列,值为实体类型名
    Core ConstructorBindingConvention 按参数名(支持 _namem_name 等变体)把构造函数参数绑定到实体属性,支持有参构造函数实例化
    Core ModelCleanupConvention FinalizeModel() 阶段清理仅构建时使用的临时状态
    Core QueryFilterRewritingConvention 对全局查询过滤器(HasQueryFilter)的表达式树做规范化重写
    Core RuntimeModelConvention 把可变模型转换为优化后的只读运行时模型,预编译各类访问器
    Relational TableNameFromDbSetConvention DbSet<T> 的属性名作为表名(如 DbSet<Order> Orders → 表名 Orders
    Relational RelationalTableAttributeConvention [Table("xxx", Schema = "yyy")] 指定表名和 schema
    Relational RelationalTableCommentAttributeConvention [Comment("xxx")] 加在类上时,设置表注释
    Relational RelationalColumnAttributeConvention [Column("xxx", TypeName = "yyy")] 指定列名和数据库类型
    Relational RelationalColumnCommentAttributeConvention [Comment("xxx")] 加在属性上时,设置列注释
    Relational SharedTableConvention 多个实体共享同一张表时(表拆分),自动调整约束/索引名以避免命名冲突
    Relational EntitySplittingConvention 管理实体拆分(一个实体映射到多张表)时各表之间的连接关系
    Relational PropertyOverridesConvention 确保属性在派生类型中的列映射覆盖(Override)与当前声明属性保持同步
    Relational EntityTypeHierarchyMappingConvention TPT(每类型一张表)时移除鉴别器列,并取消继承属性的直接映射;TPC(每具体类型一张表)时做相应处理
    Relational DiscriminatorLengthConvention 鉴别器列是 string 类型时,自动计算并设置足以容纳所有实体类型名的最大长度
    Relational CheckConstraintConvention 验证派生类型上的 CHECK 约束与基类型上的约束不冲突
    Relational RelationalValueGenerationConvention 在关系型场景下进一步细化值生成策略(如把 OnAdd 映射为数据库 IDENTITY / Sequence)
    Relational StoreGenerationConvention 防止同一列同时配置默认值和计算列(两者互斥,冲突时报错)
    Relational TableSharingConcurrencyTokenConvention 多个实体共享同一张表时,确保并发令牌在所有共享实体上都正确配置
    Relational RelationalMaxIdentifierLengthConvention 检测数据库支持的最大标识符长度,超长的对象名自动截断或哈希化
    Relational SequenceUniquificationConvention 确保同一 schema 下的序列(Sequence)名称不重复
    Relational RelationalDbFunctionAttributeConvention 标有 [DbFunction] 的静态方法自动注册为数据库函数映射
    Relational TableValuedDbFunctionConvention 可查询数据库函数(表值函数)映射的实体类型自动配置
    Relational StoredProcedureConvention 确保存储过程映射中各参数和结果列与当前属性声明保持同步
    Relational RelationalMapToJsonConvention 实体映射到 JSON 列时,自动配置默认设置(如导航属性的 JSON 元素名)
    Relational RelationalPropertyJsonPropertyNameAttributeConvention [JsonPropertyName("xxx")] 属性上指定 JSON 序列化名
    Relational RelationalNavigationJsonPropertyNameAttributeConvention [JsonPropertyName("xxx")] 导航属性上指定 JSON 元素名
    Relational RelationalKeyDiscoveryConvention 在关系型场景下补充键发现逻辑(如根据数据库列约束推断备用键)

    每个约定都实现一个事件接口(如 IEntityTypeAddedConventionIPropertyAddedConvention),在模型对象被操作时自动触发。约定之间会链式触发:RelationshipDiscoveryConvention 发现新关系 → 添加 IForeignKey → 触发 CascadeDeleteConvention 决定删除行为 → 触发 ForeignKeyIndexConvention 创建索引。

  • 第二步:Fluent API(OnModelCreating)

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>()
            .ToTable("order_table")      // 覆盖约定的表名
            .HasKey(o => o.Id)
            .Property(o => o.Status).HasMaxLength(50);
    }
    

    这些配置最终调用 IMutableEntityTypeIMutableProperty 等接口上的方法,把信息写入模型对象。

  • 第三步:冻结与运行时初始化

    FinalizeModel() 之后,模型从可写状态变成只读状态,同时把所有 C# 属性访问、值比较器等预编译成委托,避免运行时反射,提升性能。

四、谁的配置优先级最高?

约定、数据注解、Fluent API 三者都能配置同一个属性,如果它们冲突了,谁说了算?

直接说结论:Fluent API > 数据注解 > 约定,其实现方式已在上文提及。

// Metadata/ConfigurationSource.cs  用枚举来记录每个配置项是谁设的:
public enum ConfigurationSource
{
    Convention,     // 约定自动推断(优先级最低)
    DataAnnotation, // 数据注解([Key]、[Required] 等)
    Explicit        // Fluent API(优先级最高)
}

五、运行时如何使用 Metadata

Metadata 本身只是一棵被冻结的对象树,真正"让它活起来"的是 EF Core 的三大运行时管线:查询(Query)保存(SaveChanges)迁移(Migration)。它们都把 IModel 当成共享的只读"映射表"反复查询。

5.1 查询管线:把 LINQ 翻译成 SQL,再把 DataReader 还原成对象

LINQ 翻译的入口是 QueryCompilationContextsrc/EFCore/Query/QueryCompilationContext.cs),它通过依赖注入持有当前 IModel

public virtual IModel Model { get; }

接着 RelationalQueryableMethodTranslatingExpressionVisitor 把每个 DbSet<T> 翻译成 SelectExpression,过程中频繁读取 Metadata:

  • 表名 / SchemaTableExpression 直接吃 ITableBaseNameSchema 全部取自 Metadata。
  • 列名 / 可空性ColumnExpressionIColumnBase 提供,源头是 IProperty.GetColumnName()
  • 主键entityType.FindPrimaryKey()!.Properties 用来生成 JOIN 条件、分页时的稳定 ORDER BY
  • 外键:导航属性展开成 INNER/LEFT JOIN 时,从 INavigation.ForeignKeyPrincipalKey/Properties,对齐两侧列。
  • 继承判别符derivedType.GetDiscriminatorValue() 拼出 WHERE Discriminator IN (...)

SQL 执行后,ShapedQueryCompilingExpressionVisitor 负责把 DbDataReader 的每一行物化成 CLR 对象。它会查 IEntityType.ConstructorBinding 决定走"构造函数注入"还是"无参 + 属性 setter";每个 IPropertyGetGetter()/GetSetter() 已经在 FinalizeModel 阶段被编译成针对 ValueBuffer 的委托,物化时直接调用,不再反射

5.2 SaveChanges 管线:从变更追踪到 INSERT/UPDATE/DELETE

  • IKey 定位实体IdentityMap<TKey>src/EFCore/ChangeTracking/Internal/IdentityMap.cs)以 IKeyIPrincipalKeyValueFactory<TKey> 构造,字典的 EqualityComparer 来自 IProperty.GetKeyValueComparer(),因此即便是复合主键、byte[] 主键也能正确比较。

  • IProperty 比较新旧值InternalEntityEntry 通过 IProperty.GetValueComparer() 比较 OriginalValuesCurrentValues,决定 IsModified(property) 是否为 true,进而影响 UPDATE 语句要带哪些列。

  • IEntityType 生成命令CommandBatchPreparersrc/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs)遍历 entry.EntityType.GetTableMappings(),为每个映射的表创建一条 ModificationCommand。在内部按 Metadata 决定每一列的"角色":

    var isKey = property.IsPrimaryKey();
    var isCondition = isKey
        || (property.IsConcurrencyToken && storedProcedureParameter is null);
    var isWrite = ColumnModification.IsModified(entry, property);
    

    主键列 + 并发令牌列被标成 IsCondition = true,最终拼成 UPDATE … WHERE PK=… AND RowVersion=@old;执行后受影响行数为 0 即抛 DbUpdateConcurrencyExceptionUpdateSqlGenerator 只是按方言把这些 Metadata 信息拼成字符串。

5.3 FinalizeModel 之后:Runtime* 是怎么"快"起来的

ModelBuilder 阶段使用的是可变的 Model/EntityType/Property,调用 FinalizeModel() 后,它们会被冻结并转换为只读的 RuntimeModelRuntimeEntityTypeRuntimeProperty,把所有"昂贵的反射结果"预先缓存为字段:

// Metadata/RuntimeProperty.cs
private ValueComparer? _valueComparer;
private ValueComparer? _keyValueComparer;
private CoreTypeMapping? _typeMapping;
private readonly Func<IProperty, ITypeBase, ValueGenerator>? _valueGeneratorFactory;

ClrPropertyGetter / ClrPropertySetterValueComparer.EqualsExpressionValueGenerator 工厂都在 FinalizeModel 时通过表达式树编译成委托。前面提到的 IdentityMapInternalEntityEntry.IsModified、Materializer,运行时直接调用这些预编译委托即可。这就是 EF Core 在查询和保存的热路径上能接近手写 ADO.NET 性能的关键。如果再用 dotnet ef dbcontext optimize 生成 Compiled Model,连 RuntimeModel 的构建都会被提前到编译期,启动开销进一步降低。

5.4 Migration:模型差异比对也是读 Metadata

迁移并不查数据库,它比对的是两份 Metadata。MigrationsModelDiffersrc/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs)的输入是关系型投影后的 IRelationalModel(包含 Tables / Sequences / ForeignKeyConstraints 等 store-side 视图):

public virtual IReadOnlyList<MigrationOperation> GetDifferences(
    IRelationalModel? source, IRelationalModel? target)
    => Sort(Diff(source, target, new DiffContext()), diffContext);

它通过一组 Diff(IEnumerable, IEnumerable, …) 重载,逐层比对 表 → 列 → 主键 / 索引 / 外键 / 检查约束 / 序列,产出 AddColumnOperationAlterColumnOperationMigrationOperation,再交给各 Provider 的 IMigrationsSqlGenerator 翻成方言 SQL。整个过程只读 Metadata,这也是 dotnet ef migrations add 不需要连接字符串就能工作的原因。

小结

一句话总结:IModel 是一张被 Query、SaveChanges、Migration 共享的"运行时映射表"。FinalizeModel() 把模型冻成 RuntimeModel 并预编译访问器与比较器,让翻译期专注 Metadata 读取、执行期专注委托调用——前几节里那棵看似"静态"的对象树,正是在这一步真正成为整个 EF Core 的引擎中枢。

六、Metadata 目录下其他文件的作用?

前面我们通过 IModel 作为入口找到了 IEntityType 这个核心对象,再顺藤摸瓜地找到了 IPropertyIForeignKeyIIndexINavigationIKey 这几个关键成员,读到这里其实已经能够大概掌握整棵树的雏形了。但当前模块中还有几个类值得单独拿出来聊一聊,这里仅做简单介绍,感兴趣的可以进一步进行探索:

目录/文件 作用
Builders/ Fluent API 的底层实现(EntityTypeBuilderPropertyBuilder 等),你在 OnModelCreating 里写的链式调用最终都会落到这里。
Internal/ Metadata 内部实现细节,包含大量元数据对象的具体实现和构建期状态管理逻辑。
RuntimeModel.csRuntimeEntityType.csRuntimeProperty.cs 运行时只读模型对象。FinalizeModel() 之后,EF Core 会把可变模型转换成这套运行时对象,供查询与 SaveChanges 使用。
MemberIdentity.cs 统一表示“成员身份”(属性或字段),用于在模型构建时准确定位 CLR 成员。
ConfigurationSource.cs 记录配置来源(Convention / DataAnnotation / Explicit),决定冲突时谁覆盖谁。
ValueGenerated.cs 描述属性值生成策略(NeverOnAddOnAddOrUpdateOnUpdate),影响 INSERT/UPDATE 时列值的处理。
PropertySaveBehavior.cs 描述属性在保存前后的行为(Save / Ignore / Throw),用于控制某些列是否允许被客户端覆盖。
ConstructorBinding.csParameterBinding.cs 及相关 *ParameterBinding* 文件 实体构造函数参数绑定系统。EF Core 物化实体时,决定构造函数参数从哪里取值(属性值、服务、上下文等)。
RuntimeTypeMappingConfiguration.csITypeMappingConfiguration.cs 存储类型映射配置(比如精度、Unicode、长度等),供 Provider 生成正确的数据库类型。
AdHocMapper.csIAdHocMapper.cs 运行时临时类型映射支持,用于处理某些非预定义模型场景下的映射需求。

如果把 Metadata 目录按职责粗分,可以记成 4 块:

  1. 模型接口定义I*IReadOnly*IMutable*IConvention*
  2. 模型构建入口Builders/ + Conventions/
  3. 运行时模型对象Runtime*
  4. 构建与保存策略支撑ConfigurationSourceValueGeneratedPropertySaveBehavior*Binding*

七、小结

EF的代码量很大,我们没法记住全部细节,本篇分享的目标也不是介绍完所有接口的内容,而是建立三层认知即可:

  1. 结构层IModel -> IEntityType -> IProperty/IForeignKey/IIndex/IKey 这棵树长什么样。
  2. 构建层:约定 + 注解 + Fluent API 如何写入这棵树,冲突时谁覆盖谁。
  3. 运行层:查询/保存如何持续读取这棵树,并在 FinalizeModel() 后切换到 Runtime* 对象。
posted @ 2026-04-26 11:04  叨奈特挖井人  阅读(19)  评论(0)    收藏  举报