我有一堆模型,它们都利用了我创建的类。它们具有类型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所有这些属性实际上都是可绑定的,那就太好了。如果没有,那我该如何标记呢?
谢谢。
答案 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 提供程序不支持它。我不想让大量代码手动为每个道具设置值转换器和比较器。
首先使用反射获取所有相关类。然后通过查询属性(在我的例子中为 LongCollection
和 StringCollection
)来获取相关道具。最后,您可以使用 PropertyInfo
将 IMutableEntityTypeBuilder
添加到 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);
}
}
}