IMutableEntityType.GetProperties()不包含我想要的属性

时间:2019-08-28 18:05:30

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

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

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

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

然后,我的计划是在OnModelCreating()的{​​{1}}方法中,遍历我所有的实体及其属性,如果它们属于{ {1}}类型。这样,模型中的所有Measurements都将自动能够保存到数据库并从数据库中加载,并且我不必在模型中保留额外的字符串表示字段。

这是我正在谈论的循环:

DbContext

问题出在property.SetValueConverter(measurementConverter);方法上。它的文档摘要说:

  

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

其结果是我的Measurement属性根本没有返回。我相信Entity Framework认为foreach (var entity in modelBuilder.Model.GetEntityTypes()) { foreach (var property in entity.GetProperties()) { if(property.ClrType == typeof(Measurement)) { property.SetValueConverter(measurementConverter); } } } 类实际上是其自己的模型,因此它不会将其视为普通属性。当然,这意味着GetProperties()从未设置在其上,并且整个解决方案都无法按照我想要的方式工作。

我的第一个想法是将Measurement类改为一个结构,因为它已经是不可变的,并且没有真正的理由为什么它最初不是一个结构。这样做是否可以使Entity Framework清楚地知道这是一个标量属性,应该包含在Measurement返回中?我是这样认为的,但显然不是。更改后,我仍然得到相同的行为。

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

谢谢。

3 个答案:

答案 0 :(得分:2)

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

因此,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核心API都是公开可用的,并且在公共API不足的情况下可用于此类情况。唯一的区别是,在升级较新版本并进行相应更新时,您必须注意API的更改。考虑到它们破坏了公共API的次数,这不是什么大问题/关注点。

答案 1 :(得分:1)

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

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,您可以从modelBuilder中获取。

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

答案 2 :(得分:0)

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

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

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);
        }
    }
}