【问题标题】:IMutableEntityType.GetProperties() doesn't include a property that I wantIMutableEntityType.GetProperties() 不包含我想要的属性
【发布时间】:2019-08-28 18:05:30
【问题描述】:

我有一堆模型都使用了我创建的一个类。它们具有Measurement 类型的属性。这个Measurement 类本身并不是我想要映射到数据库表的实体或模型。它只是一种用于成对传递值和单位的类型,以及用于管理对这些值和单位的操作的一堆方法和运算符。

我创建了一个ValueConverter,这样我就可以在我的模型中将这种类型的属性表示为数据库中的字符串。

ValueConverter<Measurement, string> measurementConverter = new ValueConverter<Measurement, string>
(
    v => v.ToString(),
    v => new Measurement(v)
);

然后,我的计划是,在我的DbContext 中的OnModelCreating() 方法中,循环遍历我的所有实体及其属性,如果它们属于Measurement 类型,则对它们使用property.SetValueConverter(measurementConverter);。这样,我的模型中的所有测量值都将能够自动保存到数据库并从数据库中加载,并且我不必在我的模型中保留额外的字符串表示字段。

这是我正在谈论的循环:

foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
    foreach (var property in entity.GetProperties())
    {
        if(property.ClrType == typeof(Measurement)) { property.SetValueConverter(measurementConverter); }
    }
}

问题在于GetProperties() 方法。它的文档摘要是这样说的:

此 API 仅返回标量属性,不返回导航属性

结果是它根本没有返回我的Measurement 属性。我相信 Entity Framework 认为 Measurement 类实际上是它自己的模型,所以它不会把它当作一个普通的属性来对待。当然,这意味着 ValueConverter 永远不会设置在它上面,并且整个解决方案不能按照我想要的方式工作。

我的第一个想法是将Measurement 类改为结构,因为它已经是不可变的,并且没有真正的理由为什么它不能成为一个结构。这不会让实体框架清楚这是一个标量属性,应该包含在GetProperties() 返回中吗?我是这么想的,但显然不是。更改后我仍然得到相同的行为。

我想避免在每个模型中的每个 Measurement 属性上放置一个属性,所以如果有任何方法可以告诉 Entity Framework 所有这些属性实际上都是可绑定的,那就太好了。如果没有,那我该如何标记它们呢?

谢谢。

【问题讨论】:

    标签: c# asp.net-core entity-framework-core


    【解决方案1】:

    令人讨厌的是,EF Core 目前没有提供映射用户定义类型的好方法。 NET Topology套件使用了一个插件机制,它需要太多的管道代码。

    所以,GetProperties 问题:

    此 API 仅返回标量属性,不返回导航属性

    好吧,一旦您知道 EF Core 将您的类型视为实体,因此将该类型的属性视为导航属性,您就可以使用 GetNavigations 而不是 GetProperties

    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var navigation in entityType.GetNavigations().ToList())
        {
            if (navigation.ClrType == typeof(Measurement))
            {
                var entityTypeBuilder = modelBuilder.Entity(entityType.ClrType);
                entityTypeBuilder
                    .Property<Measurement>(navigation.Name)
                    .HasConversion(measurementConverter);
            }
        }
    }
    

    唯一的问题是这个

    var entityTypeBuilder = modelBuilder.Entity(entityType.ClrType);
    

    正如我们在这里看到的EF Core configuration problem with owned type used in 2 different classes,这对于拥有的实体类型是有问题的(不起作用)。让它工作需要使用内部 API:

    var entityTypeBuilder = new EntityTypeBuilder(entityType.AsEntityType().Builder);
    

    在您自己的回答中,您说使用内部 API 是“不好的”。恕我直言,唯一不好的是该库没有为此提供“好的”公共 API。另一方面,幸运的是,所有“内部”EF Core API 都是公开可用的,并且可以在公共 API 不足时用于此类场景。唯一的区别是你必须在升级新版本时观察 API 的变化并相应地更新。考虑到他们破坏公共 API 的次数,这应该不是什么大问题/担忧。

    【讨论】:

    • 哈!所以他们的想法是他们经常破坏公共 API,以至于使用内部 API 并没有真正的附加风险?我实际上想过用这段代码在 github repo 上打开一个问题。
    • 好吧,你当然可以打开一个问题。我的观点是,它不像缺乏公共内部方式来做你需要的事情那样大不了。有总比没有好:) 与需要使用反射来访问私有/内部字段等的过度封装的 EF6 相比,这是一大步。
    • 明白了!这是我第一次涉足实体框架,所以我不知道。我接受了这个答案,因为我刚刚尝试过,它的效果和我的一样好,同时也不那么复杂。
    【解决方案2】:

    现在,我并不是说这是个好主意,但我最终找到了一种方法。

    var entities = modelBuilder.Model.GetEntityTypes().ToList();
    
    for(int i = 0; i < entities.Count; i++)
    {
        var entity = entities[i];
        foreach(var property in entity.ClrType.GetProperties().Where(p => 
            p.PropertyType == typeof(Measurement) && !Attribute.IsDefined(p, typeof(NotMappedAttribute))
        ))
        {
            // This API supports the Entity Framework Core infrastructure and is not intended to be used directly from your code.
            // This API may change or be removed in future releases.
            // https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.internal.internalentitytypebuilder
    
            var entityTypeBuilderType = typeof(EntityTypeBuilder<>).MakeGenericType(entity.ClrType);
            Model model = modelBuilder.Model as Model;
            var internalModelBuilder = new InternalModelBuilder(model);
            var internalEntityTypeBuilder = new InternalEntityTypeBuilder(entity.AsEntityType(), internalModelBuilder);
            var entityTypeBuilder = (EntityTypeBuilder)Activator.CreateInstance(entityTypeBuilderType, internalEntityTypeBuilder);
            entityTypeBuilder.Property<Measurement>(property.Name).HasConversion(measurementConverter);
        }
    }
    

    这是不好的原因是因为它使用了一堆内部类。正如您在评论中看到的那样:

    此 API 支持 Entity Framework Core 基础架构,不打算直接从您的代码中使用。 此 API 可能会在未来的版本中更改或删除。

    之所以必须这样,是因为EntityTypeBuilder 必须使用您尝试配置的实体类型的泛型。 否则,以后的配置将不起作用并会报告您的实体处于“影子状态”。

    为了使用泛型,你必须使用MakeGenericType,然后是Activator.CreateInstance,然后EntityTypeBuilder需要一个InternalEntityTypeBuilder作为其构造函数的参数,以此类推......依赖项运行到Model,您可以从模型构建器中获取。

    长话短说......这很有效,但使用风险自负。

    【讨论】:

      【解决方案3】:

      我想在这里提交我自己的答案,因为我遇到了这个问题并找到了一种方法(使用一些 OP 的答案)来完成这个而不进入内部。我的用例是我需要能够存储像 long[]string[] 这样的原始集合,因为这是我正在使用的 Postgres 的一个功能,但 InMemory 提供程序不支持它。我不希望像这样为每个道具手动设置值转换器和比较器的大量代码。

      您首先使用反射获取所有相关类。然后通过查询属性(在我的例子中为LongCollectionStringCollection)来获取相关的道具。最后,您可以使用AddProperty(PropertyInfo prop)PropertyInfo 添加到IMutableEntityTypeBuilder。这是我的代码。

      protected void ConvertPrimitiveInMemoryCollections(ModelBuilder builder)
      {
          // Get all the relevant classes and map what you'll need from them
          var entities = this.GetType().Assembly
              .GetTypes()
              .Where(x => x.IsConcrete())
              // In my case, I know if it implements this interface it's relevant here
              .Where(x => typeof(IDomainEvent).IsAssignableFrom(x))
              .Select(type => new
              {
                  // This is sort of like get or add the entity to the model
                  entityTypeBuilder = builder.Model.FindEntityType(type) ?? builder.Model.AddEntityType(type),
                  stringProps = type.GetProperties()
                      .Where(prop => Attribute.IsDefined(prop, typeof(StringCollectionAttribute)))
                      .ToArray(),
                  longProps = type.GetProperties()
                      .Where(prop => Attribute.IsDefined(prop, typeof(LongCollectionAttribute)))
                      .ToArray()
              })
              .Where(x => x.stringProps.Any() || x.longProps.Any())
              .ToArray();
      
          foreach (var entity in entities)
          {
              foreach (var prop in entity.stringProps)
              {
                  var property = entity.entityTypeBuilder.AddProperty(prop);
      
                  property.SetValueConverter(StringConverter);
                  property.SetValueComparer(StringComparer);
              }
      
              foreach (var prop in entity.longProps)
              {
                  var property = entity.entityTypeBuilder.AddProperty(prop);
      
                  property.SetValueConverter(LongConverter);
                  property.SetValueComparer(LongComparer);
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2019-10-27
        • 1970-01-01
        • 1970-01-01
        • 2012-11-29
        • 1970-01-01
        • 2017-08-03
        • 1970-01-01
        • 2016-08-28
        • 1970-01-01
        相关资源
        最近更新 更多