在上一篇水文中,老周生动形象地解释了 DbContext 是如何识别实体 Set 的,大伙伴们可能会产生新的疑惑:实体是识别了,但,实体的属性或字段列表,它是怎么识别并映射给数据表的列的呢?
用过 EF 的人都知道(废话),其实默认情况下,实体类中只要不是静态的属性和字段都会被映射到数据表中,就算你不重写 DbContext 类的 OnModelCreating 方法,EF 都能自动给你“造”个模型,这是啥机制。
老周知道,大伙们比魏公公还急,那就不绕关子了,先上结论。那是因为:EF Core 中有一种类叫做约定(Conventions),或者翻译为“规范”也可以。这些约定实际上是一系列接口,但当然了,接口是不干活的,你得实现它(相信你们是学过面向老婆,哦不,是面向对象的)。为了不使大伙看得雨里云里雪里雾里,老周简单列一下常见的约定接口。这堆接口很多,也不要求你全都明白它们是什么,毕竟,咱们不会都用上的,除非你打算把 EF 的功能全部重写一遍(这样造轮子不太优雅)。
1、IConvention:所有约定接口的 base,里面是空的,仅作为一个标志——你的类如果实现了它,那表示你是一个约定。
2、IEntityTypeAddedConvention:当某个实体被添加到模型中,就会调用。
3、IPropertyAddedConvention:为实体添加属性后,就会调用。
4、IKeyAddedConvention:向实体添加主键后被调用。
5、IKeyRemovedConvention:实体主键被删除后被调用。
6、IPropertyRemovedConvention:从实体中删除某个属性后被调用。
……
这时候你会想:咦?这尼马的怎么看着那么像事件回调啊?还真是呢,这些约定接口,只要你实现了并添加到框架中,当模型发生变更后就会被调用,这使得 EF Core 能够跟踪模型的改变,保证模型状态是最新的。注意了,这里跟踪的是模型的结构(如有几个实体,实体有哪些属性会映射到数据库,有几个主键等),不是数据。
在 EF Core 内部,在初始化时会添加一些“预制菜”以供框架自己食用。即预置的约定集合,它们由一个名为 ProviderConventionSetBuilder 类负责创建。此类实现了 IProviderConventionSetBuilder 接口,继而实现了 CreateConventionSet 方法。- public virtual ConventionSet CreateConventionSet()
- {
- var conventionSet = new ConventionSet();
- conventionSet.Add(new ModelCleanupConvention(Dependencies));
- conventionSet.Add(new NotMappedTypeAttributeConvention(Dependencies));
- conventionSet.Add(new OwnedAttributeConvention(Dependencies));
- conventionSet.Add(new ComplexTypeAttributeConvention(Dependencies));
- conventionSet.Add(new KeylessAttributeConvention(Dependencies));
- conventionSet.Add(new EntityTypeConfigurationAttributeConvention(Dependencies));
- conventionSet.Add(new NotMappedMemberAttributeConvention(Dependencies));
- conventionSet.Add(new BackingFieldAttributeConvention(Dependencies));
- conventionSet.Add(new ConcurrencyCheckAttributeConvention(Dependencies));
- conventionSet.Add(new DatabaseGeneratedAttributeConvention(Dependencies));
- conventionSet.Add(new RequiredPropertyAttributeConvention(Dependencies));
- conventionSet.Add(new MaxLengthAttributeConvention(Dependencies));
- conventionSet.Add(new StringLengthAttributeConvention(Dependencies));
- conventionSet.Add(new TimestampAttributeConvention(Dependencies));
- conventionSet.Add(new ForeignKeyAttributeConvention(Dependencies));
- conventionSet.Add(new UnicodeAttributeConvention(Dependencies));
- conventionSet.Add(new PrecisionAttributeConvention(Dependencies));
- conventionSet.Add(new InversePropertyAttributeConvention(Dependencies));
- conventionSet.Add(new DeleteBehaviorAttributeConvention(Dependencies));
- conventionSet.Add(new NavigationBackingFieldAttributeConvention(Dependencies));
- conventionSet.Add(new RequiredNavigationAttributeConvention(Dependencies));
- conventionSet.Add(new NavigationEagerLoadingConvention(Dependencies));
- conventionSet.Add(new DbSetFindingConvention(Dependencies));
- conventionSet.Add(new BaseTypeDiscoveryConvention(Dependencies));
- conventionSet.Add(new ManyToManyJoinEntityTypeConvention(Dependencies));
- conventionSet.Add(new<em><strong> PropertyDiscoveryConvention</strong></em>(Dependencies));
- conventionSet.Add(new<em><strong> KeyDiscoveryConvention</strong></em>(Dependencies));
- conventionSet.Add(new ServicePropertyDiscoveryConvention(Dependencies));
- conventionSet.Add(new RelationshipDiscoveryConvention(Dependencies));
- conventionSet.Add(new ComplexPropertyDiscoveryConvention(Dependencies));
- conventionSet.Add(new ValueGenerationConvention(Dependencies));
- conventionSet.Add(new DiscriminatorConvention(Dependencies));
- conventionSet.Add(new CascadeDeleteConvention(Dependencies));
- conventionSet.Add(new ChangeTrackingStrategyConvention(Dependencies));
- conventionSet.Add(new ConstructorBindingConvention(Dependencies));
- conventionSet.Add(new KeyAttributeConvention(Dependencies));
- conventionSet.Add(new IndexAttributeConvention(Dependencies));
- conventionSet.Add(new ForeignKeyIndexConvention(Dependencies));
- conventionSet.Add(new ForeignKeyPropertyDiscoveryConvention(Dependencies));
- conventionSet.Add(new NonNullableReferencePropertyConvention(Dependencies));
- conventionSet.Add(new NonNullableNavigationConvention(Dependencies));
- conventionSet.Add(new BackingFieldConvention(Dependencies));
- conventionSet.Add(new QueryFilterRewritingConvention(Dependencies));
- conventionSet.Add(new RuntimeModelConvention(Dependencies));
- conventionSet.Add(new ElementMappingConvention(Dependencies));
- conventionSet.Add(new ElementTypeChangedConvention(Dependencies));
- return conventionSet;
- }
复制代码 好家伙,这么多。这里面有几位明星跟咱们今天的主题相关(高亮显示,被锥光灯对着那几位)。下面老详细但不啰嗦地介绍一下约定集合 ConventionSet。
这个类里面,为上面所列的接口(当然上面只列了常用的)各自分配一个 List 类型的属性。- public class ConventionSet
- {
- /// <summary>
- /// Conventions to run to setup the initial model.
- /// </summary>
- public virtual List<IModelInitializedConvention> ModelInitializedConventions { get; } = [];
- /// <summary>
- /// Conventions to run when model building is completed.
- /// </summary>
- public virtual List<IModelFinalizingConvention> ModelFinalizingConventions { get; } = [];
- /// <summary>
- /// Conventions to run when model validation is completed.
- /// </summary>
- public virtual List<IModelFinalizedConvention> ModelFinalizedConventions { get; } = [];
- ……
- /// <summary>
- /// Conventions to run when a type is ignored.
- /// </summary>
- public virtual List<ITypeIgnoredConvention> TypeIgnoredConventions { get; } = [];
- /// <summary>
- /// Conventions to run when an entity type is added to the model.
- /// </summary>
- public virtual List<IEntityTypeAddedConvention> EntityTypeAddedConventions { get; } = [];
- /// <summary>
- /// Conventions to run when an entity type is removed.
- /// </summary>
- public virtual List<IEntityTypeRemovedConvention> EntityTypeRemovedConventions { get; } = [];
- /// <summary>
- /// Conventions to run when a property is ignored.
- /// </summary>
- public virtual List<IEntityTypeMemberIgnoredConvention> EntityTypeMemberIgnoredConventions { get; } = [];
- ……
- /// <summary>
- /// Conventions to run when a primary key is changed.
- /// </summary>
- public virtual List<IEntityTypePrimaryKeyChangedConvention> EntityTypePrimaryKeyChangedConventions { get; } = [];
- /// <summary>
- /// Conventions to run when an annotation is set or removed on an entity type.
- /// </summary>
- public virtual List<IEntityTypeAnnotationChangedConvention> EntityTypeAnnotationChangedConventions { get; } = [];
- /// <summary>
- /// Conventions to run when a property is ignored.
- /// </summary>
- public virtual List<IComplexTypeMemberIgnoredConvention> ComplexTypeMemberIgnoredConventions { get; } = [];
- ……
- /// <summary>
- /// Conventions to run when an annotation is changed on the element of a collection.
- /// </summary>
- public virtual List<IElementTypeAnnotationChangedConvention> ElementTypeAnnotationChangedConventions { get; } = [];
- ……
- }
复制代码 太长了,老周省略了部分代码,反正各位知道这个规律就行。当调用 Add 方法向集合添加约定时,它会根据你的约定类所实现的接口来分类,添加到对应的 List 中。- public virtual void Add(IConvention convention)
- {
- // 实现了 IModelInitializedConvention 接口的类,初始化模型时调用
- if (convention is IModelInitializedConvention modelInitializedConvention)
- {
- ModelInitializedConventions.Add(modelInitializedConvention);
- }
- // 实现了IModelFinalizingConvention接口的类,在模型初始化之前调用
- if (convention is IModelFinalizingConvention modelFinalizingConvention)
- {
- ModelFinalizingConventions.Add(modelFinalizingConvention);
- }
- // 初始化之后调用
- if (convention is IModelFinalizedConvention modelFinalizedConvention)
- {
- ModelFinalizedConventions.Add(modelFinalizedConvention);
- }
- ……
- // 实体类型被添加到模型后调用
- if (convention is IEntityTypeAddedConvention entityTypeAddedConvention)
- {
- EntityTypeAddedConventions.Add(entityTypeAddedConvention);
- }
- // 实体从模型中删除后调用
- if (convention is IEntityTypeRemovedConvention entityTypeRemovedConvention)
- {
- EntityTypeRemovedConventions.Add(entityTypeRemovedConvention);
- }
- if (convention is IEntityTypeMemberIgnoredConvention entityTypeMemberIgnoredConvention)
- {
- EntityTypeMemberIgnoredConventions.Add(entityTypeMemberIgnoredConvention);
- }
- ……
- }
复制代码 不管是“预制”的约定,还是咱们自己定义的,都可以添加到此集合中。
现在约定集合有了,怎么让它运作起来呢?EF Core 整了个调度器—— ConventionDispatcher,该类中公开一系列 OnXXXX 方法,对应着模型的各种行为。比如,OnModelInitialized 方法在模型完成初始化后被 Model 类调用,此方法会调用约定集合中所有实现了 IModelInitializedConvention 接口的约定类。- _modelBuilderConventionContext.ResetState(modelBuilder);
- foreach (var modelConvention in conventionSet.ModelInitializedConventions)
- {
- modelConvention.ProcessModelInitialized(modelBuilder, _modelBuilderConventionContext);
- if (_modelBuilderConventionContext.ShouldStopProcessing())
- {
- return _modelBuilderConventionContext.Result!;
- }
- }
复制代码 当然,这里头很复杂,ConventionDispatcher 这些方法并非直接实现,而是嵌套了几个内部类,这些类实现 ConventionScope 抽象类。即 ImmediateConventionScope 和 DelayedConventionScope。这些类同样公开了 OnXXXX 方法。也就是说,ConventionDispatcher 类的 OnXXX 方法调用了这两个嵌套类的 OnXXXX 方法。
前文提到,OnModelInitialized 方法中通过 foreach 循环调用所有实现了 IModelInitializedConvention 接口的约定类。而 DbSetFindingConvention 类正是实现了该接口,在 ProcessModelInitialized 方法的实现中,通过 SetFinder 对象,进而将 DbContext 子类的 DbSet 类型属性所对应的实体类添加到 Model 中。- public virtual void ProcessModelInitialized(
- IConventionModelBuilder modelBuilder,
- IConventionContext<IConventionModelBuilder> context)
- {
- foreach (var setInfo in<em><strong> Dependencies.SetFinder.FindSets</strong></em>(Dependencies.ContextType))
- {
- modelBuilder.Entity(setInfo.Type, fromDataAnnotation: true);
- }
- }
复制代码 注意上面的 Dependencies.SetFinder.FindSets,咱们看看它里面是如何获得实体类型信息的。- public virtual IReadOnlyList<DbSetProperty> FindSets(Type contextType)
- => _cache.GetOrAdd(contextType, FindSetsNonCached);
- private static DbSetProperty[] FindSetsNonCached(Type contextType)
- {
- var factory = ClrPropertySetterFactory.Instance;
- return contextType.GetRuntimeProperties()
- .Where(
- <strong>p </strong><strong>=> !p.IsStatic()
- </strong>&& !&& p.DeclaringType != typeof&&&& p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
- .OrderBy(p => p.Name)
- .Select(
- p => new DbSetProperty(
- p.Name,
- p.PropertyType.GenericTypeArguments.Single(),
- p.SetMethod == null ? null : factory.Create(p)))
- .ToArray();
- }
复制代码 其实就是从 DbContext 的子类中查找符合以下条件的属性:
1、非静态成员;
2、非索引;
3、属性的类型是泛型;
4、这个泛型类是 DbSet。
老周就不继续套了,不然大伙们会头晕的,这里老周直接简单说一下这个调用链:
--> DbContext以及数据库提供者初始化
--> DbContextServices从服务容器中被提取
--> 访问 DbContextServices的 Model 或 DesignTimeModel 属性以获得 Model 对象
--> 如果 Model 未实例化则调用 CreateModel 方法
--> 先从 DbContextOptions 中找 Model(实际通过 CoreOptionsExtension 类的 Model 属性获取);
--> DbContextOptions 中未找到 Model,则从 DbContext 子类所在的程序集中查找 DbContextModelAttribute 特性,此特应用在程序集上,用于描述自定义 Model 的类型(也就是说你可以自己实现 IModel 接口,把整个 EF Core 框架的模型管理机制替换掉);
--> 如果在 DbContext 子类所在的程序集还是找不到 Model,那就用 ModelSource 类去找;
--> ModelSource 先从缓存的对象中查查有没有现成的 Model;
--> 缓存中找不到现存的 Model,认栽了,那就 new 一个;
--> new 一个 ModelConfigurationBuilder 实例;
--> 调用 DbContext 子类的 ConfigureConventions 方法。这个方法是虚的,默认是空。你在继承 DbContext 类时可以重写此方法,添加自定义的约定类;
--> new 一个 ModelBuilder 实例,调用 DbContext 子类的 OnModelCreating 方法。你在继承 DbContext 类时可以重写此方法,自己去定义模型结构。这个相信大伙伴很常用也很熟悉的套路了;
--> 最后通过 ModelBuilder.Model 属性就能获取到 Model 实例了。
在以上过程中,各种预置的约定类会被调用,当然包括 DbSetFindingConvention 类啦。
现在,大伙大概知道 DbContext 公开 DbSet,到这些 DbSet 被添加到模型的原理。既然实体类型是通过 DbSetFindingConvention 约定类添加到模型中的,那么咱们可以推测到,实体类的属性也是通过约定自动添加到模型中的,对应的约定就是 PropertyDiscoveryConvention 类。
啊,What the KAO!前面讲了这么多铺垫的话,终于轮到主角出场了。PropertyDiscoveryConvention 类的默认实现中,只要实体类中非静态的属性和字段都会被添加到模型中,从而会被映射到数据库中。
如果我们不希望某个属性被映射,最简单的方法是在这个属性(或字段,甚至整个实体类)上应用 NotMappedAttribute 就行了。不过,如果被排除的属性是具有共性的呢,总不能你每个实体类中都放一次 NotMapped 特性吧。为了好理解,老周下面用示例来说明。假设咱们的项目有这么一条规则:实体类中不管是属性还是字段,凡是带下画线开头的都不能映射到数据库中,即,如 _What、__What 之类命名的都被排除。这种情况下,一个个地做模型配置会很麻烦,就得用上约定了,只要向模型添加新实体,约定就会自动运行,排除下画线开头的成员。
咱们不需要全新造轮子,所以,最好的方案是从 PropertyDiscoveryConvention 派生。PropertyDiscoveryConvention 类公开一个虚方法叫 DiscoverPrimitiveProperties,分析属性(或字段)时否要添加到模型的逻辑都在该方法中实现的。所以,老周这里不用官方文档的方法(官方示例重写了多个方法,并且有的代码是从基类拷贝过去的),而是直接重写 DiscoverPrimitiveProperties 方法,找到下画线开头的属性,然后忽略掉即可。- public class CustPropertyDiscoveryConvention : PropertyDiscoveryConvention
- {
- // 注意,基类构造函数需要 ProviderConventionSetBuilderDependencies 类型的参数,所以我们要定义这个构造函数
- public CustPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies deps)
- :base(deps)
- {
- }
- protected override void DiscoverPrimitiveProperties(IConventionTypeBaseBuilder structuralTypeBuilder, IConventionContext context)
- {
- // 获取CLR类型
- Type clrtype = structuralTypeBuilder.Metadata.ClrType;
- // 找出带“_”开头的属性
- var props = clrtype.GetRuntimeProperties()
- .<strong>Where(p </strong><strong>=> p.Name.StartsWith("_"</strong><strong>))</strong>;
- foreach(PropertyInfo p in props)
- {
- // 这些属性忽略
- // 再调用基类成员,执行“预制”的约定,这时,被忽略的成员不再添加到模型
- base.DiscoverPrimitiveProperties(structuralTypeBuilder, context);
- }
- }
复制代码 然后,咱们定义一个实体类来测试一下。- public class Person
- {
- public int Id { get; set; }
- public string Name { get; set; } = string.Empty;
- public int? Age { get; set; }
- public string? _WhatIsThis { get; set; }
- }
复制代码 注意那个 _WhatIsThis 属性,按照本例规则,它无缘映射到数据库。
从 DbContext 类派生一个类。- public class TestDbContext : DbContext
- {
- /// <summary>
- /// 用户访问实体
- /// </summary>
- public DbSet<Person> Persons { get; set; }
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- // 配置数据库连接
- optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Demo;Integrated Security=True;Trust Server Certificate=True");
- }
- protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
- {
- configurationBuilder.<strong>Conventions.Replace</strong><strong><PropertyDiscoveryConvention></strong>(sp =>
- {
- // 1、获取 ProviderConventionSetBuilderDependencies 服务实例,因为构造函数需要它
- var deps = sp.GetRequiredService<ProviderConventionSetBuilderDependencies>();
- // 2、返回自定义属性发现约定的实例
- <strong>return new</strong><strong> CustPropertyDiscoveryConvention(deps)</strong>;
- });
- }
- }
复制代码 这里老周调用了 Replace 方法,注意泛型参数一定要指定基类 PropertyDiscoveryConvention,因为 EF Core 默认注册的类型是 PropertyDiscoveryConvention,而不是咱们自定义的 CustPropertyDiscoveryConvention。这里就是把默认的约定替换成咱们自己的。另外,你也可能不调用 Replace 方法,而是 Add 方法直接添加一个新的约定。这样做也是可以的,只不过 PropertyDiscoveryConvention 的 DiscoverPrimitiveProperties 方法会处理两次。其实影响也不大。
最后,咱们实例化数据库上下文,并创建一个数据库。- using (var dc = new TestDbContext())
- {
- dc.Database.EnsureCreated();
- // 输出模型的调试信息
- Console.WriteLine(dc.Model.ToDebugString(MetadataDebugStringOptions.ShortDefault));
- }
复制代码 运行程序代码,看到输出的模型信息中未包含 _WhatIsThis 属性。
再看看创建的数据库,表中也是没有下画线开头的列。
这就表明咱们自己的约定类被成功执行了。
咱们知道,EF Core 不仅会自动发现实体的属性,同时也会根据属性的命名自动识别主键。如你的实体类名为 Song,如果你的实体中有个属性叫 Id,或叫 SongId,类型是Guid、int 之类的类型,那这个属性会被自动标记为主键。
有了上面的认知,咱们也很快猜出来,还是预置约定干的活。对的,它叫 KeyDiscoveryConvention。如果你要对自动发现主键做定制化处理,为了便于批量应用于实体,也可以从 KeyDiscoveryConvention 派生一个类来搞搞,然后替换或添加到约定集合中即可。就像上面的示例一样,如法炮制,套路都一样的。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |