找回密码
 立即注册
首页 业界区 业界 【EF Core】框架是如何识别实体类的属性和主键的 ...

【EF Core】框架是如何识别实体类的属性和主键的

遗憩 10 小时前
在上一篇水文中,老周生动形象地解释了 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 方法。
  1.    public virtual ConventionSet CreateConventionSet()
  2.    {
  3.        var conventionSet = new ConventionSet();
  4.        conventionSet.Add(new ModelCleanupConvention(Dependencies));
  5.        conventionSet.Add(new NotMappedTypeAttributeConvention(Dependencies));
  6.        conventionSet.Add(new OwnedAttributeConvention(Dependencies));
  7.        conventionSet.Add(new ComplexTypeAttributeConvention(Dependencies));
  8.        conventionSet.Add(new KeylessAttributeConvention(Dependencies));
  9.        conventionSet.Add(new EntityTypeConfigurationAttributeConvention(Dependencies));
  10.        conventionSet.Add(new NotMappedMemberAttributeConvention(Dependencies));
  11.        conventionSet.Add(new BackingFieldAttributeConvention(Dependencies));
  12.        conventionSet.Add(new ConcurrencyCheckAttributeConvention(Dependencies));
  13.        conventionSet.Add(new DatabaseGeneratedAttributeConvention(Dependencies));
  14.        conventionSet.Add(new RequiredPropertyAttributeConvention(Dependencies));
  15.        conventionSet.Add(new MaxLengthAttributeConvention(Dependencies));
  16.        conventionSet.Add(new StringLengthAttributeConvention(Dependencies));
  17.        conventionSet.Add(new TimestampAttributeConvention(Dependencies));
  18.        conventionSet.Add(new ForeignKeyAttributeConvention(Dependencies));
  19.        conventionSet.Add(new UnicodeAttributeConvention(Dependencies));
  20.        conventionSet.Add(new PrecisionAttributeConvention(Dependencies));
  21.        conventionSet.Add(new InversePropertyAttributeConvention(Dependencies));
  22.        conventionSet.Add(new DeleteBehaviorAttributeConvention(Dependencies));
  23.        conventionSet.Add(new NavigationBackingFieldAttributeConvention(Dependencies));
  24.        conventionSet.Add(new RequiredNavigationAttributeConvention(Dependencies));
  25.        conventionSet.Add(new NavigationEagerLoadingConvention(Dependencies));
  26.        conventionSet.Add(new DbSetFindingConvention(Dependencies));
  27.        conventionSet.Add(new BaseTypeDiscoveryConvention(Dependencies));
  28.        conventionSet.Add(new ManyToManyJoinEntityTypeConvention(Dependencies));
  29.        conventionSet.Add(new<em><strong> PropertyDiscoveryConvention</strong></em>(Dependencies));
  30.        conventionSet.Add(new<em><strong> KeyDiscoveryConvention</strong></em>(Dependencies));
  31.        conventionSet.Add(new ServicePropertyDiscoveryConvention(Dependencies));
  32.        conventionSet.Add(new RelationshipDiscoveryConvention(Dependencies));
  33.        conventionSet.Add(new ComplexPropertyDiscoveryConvention(Dependencies));
  34.        conventionSet.Add(new ValueGenerationConvention(Dependencies));
  35.        conventionSet.Add(new DiscriminatorConvention(Dependencies));
  36.        conventionSet.Add(new CascadeDeleteConvention(Dependencies));
  37.        conventionSet.Add(new ChangeTrackingStrategyConvention(Dependencies));
  38.        conventionSet.Add(new ConstructorBindingConvention(Dependencies));
  39.        conventionSet.Add(new KeyAttributeConvention(Dependencies));
  40.        conventionSet.Add(new IndexAttributeConvention(Dependencies));
  41.        conventionSet.Add(new ForeignKeyIndexConvention(Dependencies));
  42.        conventionSet.Add(new ForeignKeyPropertyDiscoveryConvention(Dependencies));
  43.        conventionSet.Add(new NonNullableReferencePropertyConvention(Dependencies));
  44.        conventionSet.Add(new NonNullableNavigationConvention(Dependencies));
  45.        conventionSet.Add(new BackingFieldConvention(Dependencies));
  46.        conventionSet.Add(new QueryFilterRewritingConvention(Dependencies));
  47.        conventionSet.Add(new RuntimeModelConvention(Dependencies));
  48.        conventionSet.Add(new ElementMappingConvention(Dependencies));
  49.        conventionSet.Add(new ElementTypeChangedConvention(Dependencies));
  50.        return conventionSet;
  51.    }
复制代码
好家伙,这么多。这里面有几位明星跟咱们今天的主题相关(高亮显示,被锥光灯对着那几位)。下面老详细但不啰嗦地介绍一下约定集合 ConventionSet。
这个类里面,为上面所列的接口(当然上面只列了常用的)各自分配一个 List 类型的属性。
  1. public class ConventionSet
  2. {
  3.     /// <summary>
  4.     ///     Conventions to run to setup the initial model.
  5.     /// </summary>
  6.     public virtual List<IModelInitializedConvention> ModelInitializedConventions { get; } = [];
  7.     /// <summary>
  8.     ///     Conventions to run when model building is completed.
  9.     /// </summary>
  10.     public virtual List<IModelFinalizingConvention> ModelFinalizingConventions { get; } = [];
  11.     /// <summary>
  12.     ///     Conventions to run when model validation is completed.
  13.     /// </summary>
  14.     public virtual List<IModelFinalizedConvention> ModelFinalizedConventions { get; } = [];
  15.    ……
  16.     /// <summary>
  17.     ///     Conventions to run when a type is ignored.
  18.     /// </summary>
  19.     public virtual List<ITypeIgnoredConvention> TypeIgnoredConventions { get; } = [];
  20.     /// <summary>
  21.     ///     Conventions to run when an entity type is added to the model.
  22.     /// </summary>
  23.     public virtual List<IEntityTypeAddedConvention> EntityTypeAddedConventions { get; } = [];
  24.     /// <summary>
  25.     ///     Conventions to run when an entity type is removed.
  26.     /// </summary>
  27.     public virtual List<IEntityTypeRemovedConvention> EntityTypeRemovedConventions { get; } = [];
  28.     /// <summary>
  29.     ///     Conventions to run when a property is ignored.
  30.     /// </summary>
  31.     public virtual List<IEntityTypeMemberIgnoredConvention> EntityTypeMemberIgnoredConventions { get; } = [];
  32.    ……
  33.     /// <summary>
  34.     ///     Conventions to run when a primary key is changed.
  35.     /// </summary>
  36.     public virtual List<IEntityTypePrimaryKeyChangedConvention> EntityTypePrimaryKeyChangedConventions { get; } = [];
  37.     /// <summary>
  38.     ///     Conventions to run when an annotation is set or removed on an entity type.
  39.     /// </summary>
  40.     public virtual List<IEntityTypeAnnotationChangedConvention> EntityTypeAnnotationChangedConventions { get; } = [];
  41.     /// <summary>
  42.     ///     Conventions to run when a property is ignored.
  43.     /// </summary>
  44.     public virtual List<IComplexTypeMemberIgnoredConvention> ComplexTypeMemberIgnoredConventions { get; } = [];
  45. ……
  46.     /// <summary>
  47.     ///     Conventions to run when an annotation is changed on the element of a collection.
  48.     /// </summary>
  49.     public virtual List<IElementTypeAnnotationChangedConvention> ElementTypeAnnotationChangedConventions { get; } = [];
  50.     ……
  51. }
复制代码
太长了,老周省略了部分代码,反正各位知道这个规律就行。当调用 Add 方法向集合添加约定时,它会根据你的约定类所实现的接口来分类,添加到对应的 List 中。
  1.     public virtual void Add(IConvention convention)
  2.     {
  3.         // 实现了 IModelInitializedConvention  接口的类,初始化模型时调用
  4.         if (convention is IModelInitializedConvention modelInitializedConvention)
  5.         {
  6.             ModelInitializedConventions.Add(modelInitializedConvention);
  7.         }
  8.         // 实现了IModelFinalizingConvention接口的类,在模型初始化之前调用
  9.         if (convention is IModelFinalizingConvention modelFinalizingConvention)
  10.         {
  11.             ModelFinalizingConventions.Add(modelFinalizingConvention);
  12.         }
  13.         // 初始化之后调用
  14.         if (convention is IModelFinalizedConvention modelFinalizedConvention)
  15.         {
  16.             ModelFinalizedConventions.Add(modelFinalizedConvention);
  17.         }
  18.        ……
  19.         // 实体类型被添加到模型后调用
  20.         if (convention is IEntityTypeAddedConvention entityTypeAddedConvention)
  21.         {
  22.             EntityTypeAddedConventions.Add(entityTypeAddedConvention);
  23.         }
  24.          // 实体从模型中删除后调用
  25.         if (convention is IEntityTypeRemovedConvention entityTypeRemovedConvention)
  26.         {
  27.             EntityTypeRemovedConventions.Add(entityTypeRemovedConvention);
  28.         }
  29.         if (convention is IEntityTypeMemberIgnoredConvention entityTypeMemberIgnoredConvention)
  30.         {
  31.             EntityTypeMemberIgnoredConventions.Add(entityTypeMemberIgnoredConvention);
  32.         }
  33.    ……
  34. }
复制代码
不管是“预制”的约定,还是咱们自己定义的,都可以添加到此集合中。
 
现在约定集合有了,怎么让它运作起来呢?EF Core 整了个调度器—— ConventionDispatcher,该类中公开一系列 OnXXXX 方法,对应着模型的各种行为。比如,OnModelInitialized 方法在模型完成初始化后被 Model 类调用,此方法会调用约定集合中所有实现了 IModelInitializedConvention 接口的约定类。
  1. _modelBuilderConventionContext.ResetState(modelBuilder);
  2. foreach (var modelConvention in conventionSet.ModelInitializedConventions)
  3. {
  4.      modelConvention.ProcessModelInitialized(modelBuilder, _modelBuilderConventionContext);
  5.      if (_modelBuilderConventionContext.ShouldStopProcessing())
  6.      {
  7.          return _modelBuilderConventionContext.Result!;
  8.      }
  9. }
复制代码
当然,这里头很复杂,ConventionDispatcher 这些方法并非直接实现,而是嵌套了几个内部类,这些类实现 ConventionScope 抽象类。即 ImmediateConventionScope 和 DelayedConventionScope。这些类同样公开了 OnXXXX 方法。也就是说,ConventionDispatcher 类的 OnXXX 方法调用了这两个嵌套类的 OnXXXX 方法。
前文提到,OnModelInitialized 方法中通过 foreach 循环调用所有实现了 IModelInitializedConvention 接口的约定类。而 DbSetFindingConvention 类正是实现了该接口,在 ProcessModelInitialized 方法的实现中,通过 SetFinder 对象,进而将 DbContext 子类的 DbSet 类型属性所对应的实体类添加到 Model 中。
  1.     public virtual void ProcessModelInitialized(
  2.         IConventionModelBuilder modelBuilder,
  3.         IConventionContext<IConventionModelBuilder> context)
  4.     {
  5.         foreach (var setInfo in<em><strong> Dependencies.SetFinder.FindSets</strong></em>(Dependencies.ContextType))
  6.         {
  7.             modelBuilder.Entity(setInfo.Type, fromDataAnnotation: true);
  8.         }
  9.     }
复制代码
注意上面的 Dependencies.SetFinder.FindSets,咱们看看它里面是如何获得实体类型信息的。
  1.     public virtual IReadOnlyList<DbSetProperty> FindSets(Type contextType)
  2.         => _cache.GetOrAdd(contextType, FindSetsNonCached);
  3.     private static DbSetProperty[] FindSetsNonCached(Type contextType)
  4.     {
  5.         var factory = ClrPropertySetterFactory.Instance;
  6.         return contextType.GetRuntimeProperties()
  7.             .Where(
  8.                 <strong>p </strong><strong>=> !p.IsStatic()
  9. </strong>&& !&& p.DeclaringType != typeof&&&& p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
  10.             .OrderBy(p => p.Name)
  11.             .Select(
  12.                 p => new DbSetProperty(
  13.                     p.Name,
  14.                     p.PropertyType.GenericTypeArguments.Single(),
  15.                     p.SetMethod == null ? null : factory.Create(p)))
  16.             .ToArray();
  17.     }
复制代码
其实就是从 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 方法,找到下画线开头的属性,然后忽略掉即可。
  1.     public class CustPropertyDiscoveryConvention : PropertyDiscoveryConvention
  2.     {
  3.         // 注意,基类构造函数需要 ProviderConventionSetBuilderDependencies 类型的参数,所以我们要定义这个构造函数
  4.         public CustPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies deps)
  5.             :base(deps)
  6.         {
  7.         }
  8.         protected override void DiscoverPrimitiveProperties(IConventionTypeBaseBuilder structuralTypeBuilder, IConventionContext context)
  9.         {
  10.             // 获取CLR类型
  11.             Type clrtype = structuralTypeBuilder.Metadata.ClrType;
  12.             // 找出带“_”开头的属性
  13.             var props = clrtype.GetRuntimeProperties()
  14.                                 .<strong>Where(p </strong><strong>=> p.Name.StartsWith("_"</strong><strong>))</strong>;
  15.             foreach(PropertyInfo p in props)
  16.             {
  17.                 // 这些属性忽略
  18. // 再调用基类成员,执行“预制”的约定,这时,被忽略的成员不再添加到模型
  19.             base.DiscoverPrimitiveProperties(structuralTypeBuilder, context);
  20.         }
  21.     }
复制代码
然后,咱们定义一个实体类来测试一下。
  1.     public class Person
  2.     {
  3.         public int Id { get; set; }
  4.         public string Name { get; set; } = string.Empty;
  5.         public int? Age { get; set; }
  6.         public string? _WhatIsThis { get; set; }
  7.     }
复制代码
注意那个 _WhatIsThis 属性,按照本例规则,它无缘映射到数据库。
从 DbContext 类派生一个类。
  1.     public class TestDbContext : DbContext
  2.     {
  3.         /// <summary>
  4.         /// 用户访问实体
  5.         /// </summary>
  6.         public DbSet<Person> Persons {  get; set; }
  7.         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  8.         {
  9.             // 配置数据库连接
  10.             optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Demo;Integrated Security=True;Trust Server Certificate=True");
  11.         }
  12.         protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
  13.         {
  14.             configurationBuilder.<strong>Conventions.Replace</strong><strong><PropertyDiscoveryConvention></strong>(sp =>
  15.             {
  16.                 // 1、获取 ProviderConventionSetBuilderDependencies 服务实例,因为构造函数需要它
  17.                 var deps = sp.GetRequiredService<ProviderConventionSetBuilderDependencies>();
  18.                 // 2、返回自定义属性发现约定的实例
  19.                 <strong>return new</strong><strong> CustPropertyDiscoveryConvention(deps)</strong>;
  20.             });
  21.         }
  22.     }
复制代码
这里老周调用了 Replace 方法,注意泛型参数一定要指定基类 PropertyDiscoveryConvention,因为 EF Core 默认注册的类型是 PropertyDiscoveryConvention,而不是咱们自定义的 CustPropertyDiscoveryConvention。这里就是把默认的约定替换成咱们自己的。另外,你也可能不调用 Replace 方法,而是 Add 方法直接添加一个新的约定。这样做也是可以的,只不过 PropertyDiscoveryConvention 的 DiscoverPrimitiveProperties 方法会处理两次。其实影响也不大。
最后,咱们实例化数据库上下文,并创建一个数据库。
  1.   using (var dc = new TestDbContext())
  2.   {
  3.       dc.Database.EnsureCreated();
  4.       // 输出模型的调试信息
  5.       Console.WriteLine(dc.Model.ToDebugString(MetadataDebugStringOptions.ShortDefault));
  6.   }
复制代码
运行程序代码,看到输出的模型信息中未包含 _WhatIsThis 属性。
1.png

再看看创建的数据库,表中也是没有下画线开头的列。
2.png

这就表明咱们自己的约定类被成功执行了。
 
咱们知道,EF Core 不仅会自动发现实体的属性,同时也会根据属性的命名自动识别主键。如你的实体类名为 Song,如果你的实体中有个属性叫 Id,或叫 SongId,类型是Guid、int 之类的类型,那这个属性会被自动标记为主键。
有了上面的认知,咱们也很快猜出来,还是预置约定干的活。对的,它叫 KeyDiscoveryConvention。如果你要对自动发现主键做定制化处理,为了便于批量应用于实体,也可以从 KeyDiscoveryConvention 派生一个类来搞搞,然后替换或添加到约定集合中即可。就像上面的示例一样,如法炮制,套路都一样的。
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册