我正在尝试限制仅从generic
Entities
到inherit
的{{1}}的两个IParentOf<TChildEntity>
方法,并访问interface
Entity's
(ParentId)Foreign Key
。
演示;
Generically
子实体类模型看起来像这样,
public void AdoptAll<TParentEntity, TChildEntity>(TParentEntity parent,
TParentEntity adoptee)
where TParentEntity : DataEntity, IParentOf<TChildEntity>
where TChildEntity : DataEntity, IChildOf<TParentEntity>
{
foreach (TChildEntity child in (IParentOf<TChildEntity>)parent.Children)
{
(IChildOf<TParentEntity)child.ParentId = adoptee.Id;
}
}
首先,这有可能吗?还是会在EF中崩溃?
第二,由于外键不遵循约定(并且有多个),如何通过Fluent Api设置它们?我可以在数据注释中看到如何执行此操作。
我希望这一点很清楚,我已经考虑了一段时间并尝试解决它,因此我可以遵循我的论点,但可能无法清楚地表达出来,因此请根据需要进行澄清。我这样做的原因是为了使代码安全,并使许多添加新关联和实体所需的类的手动更改自动化。
谢谢。
修改
我决定创建一些基本的类来实现这一想法并对其进行测试,我的代码如下。
public class Account : DataEntity, IChildOf<AccountType>, IChildOf<AccountData>
{
public string Name { get; set; }
public string Balance { get; set; }
// Foreign Key and Navigation Property for AccountType
int IChildOf<AccountType>.ParentId{ get; set; }
public virtual AccountType AccountType { get; set; }
// Foreign Key and Navigation Property for AccountData
int IChildOf<AccountData>.ParentId{ get; set; }
public virtual AccountData AccountData { get; set; }
}
public abstract class ChildEntity : DataEntity
{
public T GetParent<T>() where T : ParentEntity
{
foreach (var item in GetType().GetProperties())
{
if (item.GetValue(this) is T entity)
return entity;
}
return null;
}
}
public abstract class ParentEntity : DataEntity
{
public ICollection<T> GetChildren<T>() where T : ChildEntity
{
foreach (var item in GetType().GetProperties())
{
if (item.GetValue(this) is ICollection<T> collection)
return collection;
}
return null;
}
}
public interface IParent<TEntity> where TEntity : ChildEntity
{
ICollection<T> GetChildren<T>() where T : ChildEntity;
}
public interface IChild<TEntity> where TEntity : ParentEntity
{
int ForeignKey { get; set; }
T GetParent<T>() where T : ParentEntity;
}
public class ParentOne : ParentEntity, IParent<ChildOne>
{
public string Name { get; set; }
public decimal Amount { get; set; }
public virtual ICollection<ChildOne> ChildOnes { get; set; }
}
public class ParentTwo : ParentEntity, IParent<ChildOne>
{
public string Name { get; set; }
public decimal Value { get; set; }
public virtual ICollection<ChildOne> ChildOnes { get; set; }
}
public class ChildOne : ChildEntity, IChild<ParentOne>, IChild<ParentTwo>
{
public string Name { get; set; }
public decimal Balance { get; set; }
int IChild<ParentOne>.ForeignKey { get; set; }
public virtual ParentOne ParentOne { get; set; }
int IChild<ParentTwo>.ForeignKey { get; set; }
public virtual ParentTwo ParentTwo { get; set; }
}
只是给每个Data Entity
一个entity
Id
。
我有一个标准的通用存储库,其中设置了一个工作单元类来进行中介。 AdoptAll方法在我的程序中看起来像这样。
property
这似乎可以正常工作,并且没有故障(最低测试),这样做是否有重大缺陷?
谢谢。
编辑两个
这是DbContext中的OnModelCreating方法,该方法为每个实体设置外键。这有问题吗?
public void AdoptAll<TParentEntity, TChildEntity>(TParentEntity parent,
TParentEntity adoptee, UoW uoW)
where TParentEntity : DataEntity, IParent<TChildEntity>
where TChildEntity : DataEntity, IChild<TParentEntity>
{
var currentParent = uoW.GetRepository<TParentEntity>().Get(parent.Id);
foreach (TChildEntity child in currentParent.GetChildren<TChildEntity>())
{
child.ForeignKey = adoptee.Id;
}
}
答案 0 :(得分:3)
根据更新后的示例,您想从实体类公共接口隐藏显式FK,并且仍然让其对于EF Core可见并映射到数据库中的FK列。
第一个问题是,EF无法直接发现显式实现的接口成员。而且它没有好名字,因此默认约定不适用。
例如,没有流畅的配置EF Core将在Parent
和Child
实体之间正确创建一对多关联,但是由于它不会发现int IChild<Parent>.ForeignKey { get; set; }
属性,因此它可以将通过ParentOneId
/ ParentTwoId
shadow properties而不是通过接口显式属性来维护FK属性值。换句话说,这些属性将不会被EF Core填充,也不会被变更跟踪器考虑。
要让EF Core使用它们,您需要分别使用HasForeignKey
和HasColumnName
流利的API方法重载接受string
属性名称来映射FK属性和数据库列名称。请注意,字符串属性名称必须使用名称空间完全限定。尽管Type.FullName
为非泛型类型提供了该字符串,但是对于诸如IChild<ParentOne>
这样的泛型类型没有这种属性/方法(结果必须为"Namespace.IChild<Namespace.ParentOne>"
),所以让我们首先创建一些帮助器为此:
static string ChildForeignKeyPropertyName<TParent>() where TParent : ParentEntity
=> $"{typeof(IChild<>).Namespace}.IChild<{typeof(TParent).FullName}>.{nameof(IChild<TParent>.ForeignKey)}";
static string ChildForeignKeyColumnName<TParent>() where TParent : ParentEntity
=> $"{typeof(TParent).Name}Id";
接下来将创建一个用于执行必要配置的辅助方法:
static void ConfigureRelationship<TChild, TParent>(ModelBuilder modelBuilder)
where TChild : ChildEntity, IChild<TParent>
where TParent : ParentEntity, IParent<TChild>
{
var childEntity = modelBuilder.Entity<TChild>();
var foreignKeyPropertyName = ChildForeignKeyPropertyName<TParent>();
var foreignKeyColumnName = ChildForeignKeyColumnName<TParent>();
var foreignKey = childEntity.Metadata.GetForeignKeys()
.Single(fk => fk.PrincipalEntityType.ClrType == typeof(TParent));
// Configure FK column name
childEntity
.Property<int>(foreignKeyPropertyName)
.HasColumnName(foreignKeyColumnName);
// Configure FK property
childEntity
.HasOne<TParent>(foreignKey.DependentToPrincipal.Name)
.WithMany(foreignKey.PrincipalToDependent.Name)
.HasForeignKey(foreignKeyPropertyName);
}
如您所见,我正在使用EF Core提供的元数据服务来查找相应导航属性的名称。
但是这种通用方法实际上表明了这种设计的局限性。通用约束使我们可以使用
childEntity.Property(c => c.ForeignKey)
,可以正常编译,但在运行时不起作用。它不仅适用于流利的API方法,而且基本上适用于任何涉及表达式树的通用方法(例如LINQ to Entities查询)。当接口属性与公共属性隐式实现时,就没有这种问题。
我们稍后将返回此限制。要完成映射,请将以下内容添加到您的OnModelCreating
替代项中:
ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);
现在,EF Core将正确加载/考虑您明确实现的FK属性。
现在回到限制。使用通用对象服务(如AdoptAll
方法或LINQ to Objects)没有问题。但是,您无法在用于访问EF Core元数据的表达式中或LINQ to Entities查询中通用访问这些属性。在后一种情况下,您应该通过导航属性访问它,或者在两种情况下,都应该使用从ChildForeignKeyPropertyName<TParent>()
方法返回的名称进行访问。实际上,查询可以工作,但将被评估locally,从而导致性能问题或意外行为。
例如
static IEnumerable<TChild> GetChildrenOf<TChild, TParent>(DbContext db, int parentId)
where TChild : ChildEntity, IChild<TParent>
where TParent : ParentEntity, IParent<TChild>
{
// Works, but causes client side filter evalution
return db.Set<TChild>().Where(c => c.ForeignKey == parentId);
// This correctly translates to SQL, hence server side evaluation
return db.Set<TChild>().Where(c => EF.Property<int>(c, ChildForeignKeyPropertyName<TParent>()) == parentId);
}
简而言之,这是可能的,但请谨慎使用,并确保它在允许的有限通用服务场景中值得使用。替代方法将不使用接口,而是使用EF Core元数据,(反射)或Func<...>
/ Expression<Func<..>>
通用方法参数(与Queryable
扩展方法类似)的组合。
编辑:关于第二个问题,流畅的配置
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentOne)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentOne>)fk).ForeignKey);
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentTwo)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentTwo>)fk).ForeignKey);
为ChildOne
migrationBuilder.CreateTable(
name: "ChildOne",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
ForeignKey = table.Column<int>(nullable: false),
Name = table.Column<string>(nullable: true),
Balance = table.Column<decimal>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChildOne", x => x.Id);
table.ForeignKey(
name: "FK_ChildOne_ParentOne_ForeignKey",
column: x => x.ForeignKey,
principalTable: "ParentOne",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChildOne_ParentTwo_ForeignKey",
column: x => x.ForeignKey,
principalTable: "ParentTwo",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
请注意单个ForeignKey
列,并尝试将其用作ParentOne
和ParentTwo
的外键。与直接使用受约束的接口属性一样,它也会遇到同样的问题,因此我认为它不起作用。