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 时拿到的都是只读视图(IEntityType、IProperty)。 -
约定阶段需要"记录是谁写的":框架约定(Convention)、数据注解(DataAnnotation)、Fluent API 三者都能配置同一个属性,必须有机制来仲裁谁说了算。
IConvention*的每次写入都会附带一个ConfigurationSource(枚举值 0/1/2),后来的配置只有在来源优先级大于等于之前的值时才允许覆盖,这是"可追溯、可仲裁"的关键。 -
Fluent API 只需要纯粹的写入能力:
OnModelCreating里的配置被标记为Explicit,优先级最高,不需要参与约定系统的仲裁流程,所以用更简单的IMutable*即可。 -
Finalize 后切换为运行时专属对象:
FinalizeModel()会把可变的构建期元数据转换为RuntimeEntityType、RuntimeProperty等只读对象,同时把属性访问、值比较器预编译成委托,运行期直接读这套对象,不再反射,性能更好。
每个类型都有自己专属的职责,下面逐一说明:
-
IProperty — 列的相关配置
查询时选哪些列、SaveChanges 时怎么处理这一列的值、变更追踪时怎么比较新旧值——全都读
IProperty。IsNullable:这一列允不允许 NULLValueGenerated:值是自己填(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.Customer和Customer.Orders)IsUnique:是否是一对一关系DeleteBehavior:级联删除策略(置 NULL、报错等)
-
IIndex — 数据库索引的描述
IIndex记录了要在哪些列上建索引,在 Migration 阶段使用——IMigrationsModelDiffer对比模型差异时,会检查索引是否需要新增或删除,生成对应的CREATE INDEX/DROP INDEX脚本。运行时查询翻译不直接用它,索引的生效与否是数据库自己的事。Properties:构成索引的列(支持复合索引)IsUnique:是否是唯一索引Name:索引名称
三、模型是怎么构建出来的?
明白了 Metadata 是什么之后,下一个问题是:这棵对象树是什么时候、怎么被填充起来的?
入口是 ModelSource(src/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按命名规则( _name、m_name、name_等)自动发现属性的后备字段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)自动设置级联删除;可选关系设为置 NULLCore DeleteBehaviorAttributeConvention导航属性上的 [DeleteBehavior(xxx)]配置删除行为Core RequiredNavigationAttributeConvention导航属性上的 [Required]将主体端配置为必填Core NavigationEagerLoadingConvention从属实体(Owned Type)的导航属性自动配置为急加载 Core NonNullableNavigationConvention开启 C# 可空引用类型时,非空导航属性自动标记为必填关系 Core RequiredPropertyAttributeConvention[Required]属性自动标记为不可为 NULLCore 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按参数名(支持 _name、m_name等变体)把构造函数参数绑定到实体属性,支持有参构造函数实例化Core ModelCleanupConventionFinalizeModel()阶段清理仅构建时使用的临时状态Core QueryFilterRewritingConvention对全局查询过滤器( HasQueryFilter)的表达式树做规范化重写Core RuntimeModelConvention把可变模型转换为优化后的只读运行时模型,预编译各类访问器 Relational TableNameFromDbSetConvention用 DbSet<T>的属性名作为表名(如DbSet<Order> Orders→ 表名Orders)Relational RelationalTableAttributeConvention[Table("xxx", Schema = "yyy")]指定表名和 schemaRelational RelationalTableCommentAttributeConvention[Comment("xxx")]加在类上时,设置表注释Relational RelationalColumnAttributeConvention[Column("xxx", TypeName = "yyy")]指定列名和数据库类型Relational RelationalColumnCommentAttributeConvention[Comment("xxx")]加在属性上时,设置列注释Relational SharedTableConvention多个实体共享同一张表时(表拆分),自动调整约束/索引名以避免命名冲突 Relational EntitySplittingConvention管理实体拆分(一个实体映射到多张表)时各表之间的连接关系 Relational PropertyOverridesConvention确保属性在派生类型中的列映射覆盖(Override)与当前声明属性保持同步 Relational EntityTypeHierarchyMappingConventionTPT(每类型一张表)时移除鉴别器列,并取消继承属性的直接映射;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在关系型场景下补充键发现逻辑(如根据数据库列约束推断备用键) 每个约定都实现一个事件接口(如
IEntityTypeAddedConvention、IPropertyAddedConvention),在模型对象被操作时自动触发。约定之间会链式触发: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); }这些配置最终调用
IMutableEntityType、IMutableProperty等接口上的方法,把信息写入模型对象。 -
第三步:冻结与运行时初始化
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 翻译的入口是 QueryCompilationContext(src/EFCore/Query/QueryCompilationContext.cs),它通过依赖注入持有当前 IModel:
public virtual IModel Model { get; }
接着 RelationalQueryableMethodTranslatingExpressionVisitor 把每个 DbSet<T> 翻译成 SelectExpression,过程中频繁读取 Metadata:
- 表名 / Schema:
TableExpression直接吃ITableBase,Name、Schema全部取自 Metadata。 - 列名 / 可空性:
ColumnExpression由IColumnBase提供,源头是IProperty.GetColumnName()。 - 主键:
entityType.FindPrimaryKey()!.Properties用来生成 JOIN 条件、分页时的稳定ORDER BY。 - 外键:导航属性展开成 INNER/LEFT JOIN 时,从
INavigation.ForeignKey读PrincipalKey/Properties,对齐两侧列。 - 继承判别符:
derivedType.GetDiscriminatorValue()拼出WHERE Discriminator IN (...)。
SQL 执行后,ShapedQueryCompilingExpressionVisitor 负责把 DbDataReader 的每一行物化成 CLR 对象。它会查 IEntityType.ConstructorBinding 决定走"构造函数注入"还是"无参 + 属性 setter";每个 IProperty 的 GetGetter()/GetSetter() 已经在 FinalizeModel 阶段被编译成针对 ValueBuffer 的委托,物化时直接调用,不再反射。
5.2 SaveChanges 管线:从变更追踪到 INSERT/UPDATE/DELETE
-
用
IKey定位实体:IdentityMap<TKey>(src/EFCore/ChangeTracking/Internal/IdentityMap.cs)以IKey加IPrincipalKeyValueFactory<TKey>构造,字典的EqualityComparer来自IProperty.GetKeyValueComparer(),因此即便是复合主键、byte[]主键也能正确比较。 -
用
IProperty比较新旧值:InternalEntityEntry通过IProperty.GetValueComparer()比较OriginalValues和CurrentValues,决定IsModified(property)是否为true,进而影响 UPDATE 语句要带哪些列。 -
按
IEntityType生成命令:CommandBatchPreparer(src/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 即抛DbUpdateConcurrencyException。UpdateSqlGenerator只是按方言把这些 Metadata 信息拼成字符串。
5.3 FinalizeModel 之后:Runtime* 是怎么"快"起来的
ModelBuilder 阶段使用的是可变的 Model/EntityType/Property,调用 FinalizeModel() 后,它们会被冻结并转换为只读的 RuntimeModel、RuntimeEntityType、RuntimeProperty,把所有"昂贵的反射结果"预先缓存为字段:
// Metadata/RuntimeProperty.cs
private ValueComparer? _valueComparer;
private ValueComparer? _keyValueComparer;
private CoreTypeMapping? _typeMapping;
private readonly Func<IProperty, ITypeBase, ValueGenerator>? _valueGeneratorFactory;
ClrPropertyGetter / ClrPropertySetter、ValueComparer.EqualsExpression、ValueGenerator 工厂都在 FinalizeModel 时通过表达式树编译成委托。前面提到的 IdentityMap、InternalEntityEntry.IsModified、Materializer,运行时直接调用这些预编译委托即可。这就是 EF Core 在查询和保存的热路径上能接近手写 ADO.NET 性能的关键。如果再用 dotnet ef dbcontext optimize 生成 Compiled Model,连 RuntimeModel 的构建都会被提前到编译期,启动开销进一步降低。
5.4 Migration:模型差异比对也是读 Metadata
迁移并不查数据库,它比对的是两份 Metadata。MigrationsModelDiffer(src/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, …) 重载,逐层比对 表 → 列 → 主键 / 索引 / 外键 / 检查约束 / 序列,产出 AddColumnOperation、AlterColumnOperation 等 MigrationOperation,再交给各 Provider 的 IMigrationsSqlGenerator 翻成方言 SQL。整个过程只读 Metadata,这也是 dotnet ef migrations add 不需要连接字符串就能工作的原因。
小结
一句话总结:IModel 是一张被 Query、SaveChanges、Migration 共享的"运行时映射表"。FinalizeModel() 把模型冻成 RuntimeModel 并预编译访问器与比较器,让翻译期专注 Metadata 读取、执行期专注委托调用——前几节里那棵看似"静态"的对象树,正是在这一步真正成为整个 EF Core 的引擎中枢。
六、Metadata 目录下其他文件的作用?
前面我们通过 IModel 作为入口找到了 IEntityType 这个核心对象,再顺藤摸瓜地找到了 IProperty、IForeignKey、IIndex、INavigation、IKey 这几个关键成员,读到这里其实已经能够大概掌握整棵树的雏形了。但当前模块中还有几个类值得单独拿出来聊一聊,这里仅做简单介绍,感兴趣的可以进一步进行探索:
| 目录/文件 | 作用 |
|---|---|
Builders/ |
Fluent API 的底层实现(EntityTypeBuilder、PropertyBuilder 等),你在 OnModelCreating 里写的链式调用最终都会落到这里。 |
Internal/ |
Metadata 内部实现细节,包含大量元数据对象的具体实现和构建期状态管理逻辑。 |
RuntimeModel.cs、RuntimeEntityType.cs、RuntimeProperty.cs 等 |
运行时只读模型对象。FinalizeModel() 之后,EF Core 会把可变模型转换成这套运行时对象,供查询与 SaveChanges 使用。 |
MemberIdentity.cs |
统一表示“成员身份”(属性或字段),用于在模型构建时准确定位 CLR 成员。 |
ConfigurationSource.cs |
记录配置来源(Convention / DataAnnotation / Explicit),决定冲突时谁覆盖谁。 |
ValueGenerated.cs |
描述属性值生成策略(Never、OnAdd、OnAddOrUpdate、OnUpdate),影响 INSERT/UPDATE 时列值的处理。 |
PropertySaveBehavior.cs |
描述属性在保存前后的行为(Save / Ignore / Throw),用于控制某些列是否允许被客户端覆盖。 |
ConstructorBinding.cs、ParameterBinding.cs 及相关 *ParameterBinding* 文件 |
实体构造函数参数绑定系统。EF Core 物化实体时,决定构造函数参数从哪里取值(属性值、服务、上下文等)。 |
RuntimeTypeMappingConfiguration.cs、ITypeMappingConfiguration.cs |
存储类型映射配置(比如精度、Unicode、长度等),供 Provider 生成正确的数据库类型。 |
AdHocMapper.cs、IAdHocMapper.cs |
运行时临时类型映射支持,用于处理某些非预定义模型场景下的映射需求。 |
如果把 Metadata 目录按职责粗分,可以记成 4 块:
- 模型接口定义:
I*、IReadOnly*、IMutable*、IConvention* - 模型构建入口:
Builders/+Conventions/ - 运行时模型对象:
Runtime* - 构建与保存策略支撑:
ConfigurationSource、ValueGenerated、PropertySaveBehavior、*Binding*
七、小结
EF的代码量很大,我们没法记住全部细节,本篇分享的目标也不是介绍完所有接口的内容,而是建立三层认知即可:
- 结构层:
IModel -> IEntityType -> IProperty/IForeignKey/IIndex/IKey这棵树长什么样。 - 构建层:约定 + 注解 + Fluent API 如何写入这棵树,冲突时谁覆盖谁。
- 运行层:查询/保存如何持续读取这棵树,并在
FinalizeModel()后切换到Runtime*对象。

浙公网安备 33010602011771号