[英]Can I use an Interface with a Foreign Key in EF Core and set it as a foreign key using Fluent API?
我試圖限制一對夫婦的generic
方法,只允許Entities
是inherit
自IParentOf<TChildEntity>
interface
,以及訪問一個Entity's
Foreign Key
(的ParentId) 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;
}
}
子實體類模型看起來像這樣,
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; }
}
首先,這可能嗎? 或者它會在EF中崩潰嗎?
其次,由於外鍵不遵循慣例(並且有多個),我如何通過Fluent Api設置它們? 我可以在Data Annotations中看到如何做到這一點。
我希望這很清楚,我一直在考慮它並試圖解決它,所以我可以按照我的論點,但可能沒有明確表達,所以如果需要請請澄清。 我想要這樣做的原因是為了使代碼安全以及自動化大量手動更改添加新關聯和實體所必需的類。
謝謝。
編輯
我決定創建一些基本類來實現這個想法並測試它,我的代碼如下。
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
property
。
我有標准的通用存儲庫,其中設置了一個工作單元類用於調解。 AdoptAll方法在我的程序中看起來像這樣。
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;
}
}
這似乎工作正常,沒有錯誤(最小測試)這樣做有什么重大缺陷嗎?
謝謝。
編輯二
這是DbContext中的OnModelCreating方法,它為每個實體設置外鍵。 這有問題嗎?
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
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);
}
根據更新的示例,您希望隱藏實體類公共接口中的顯式FK,並且仍然允許它對EF Core可見並映射到數據庫中的FK列。
第一個問題是EF無法直接發現明確實現的接口成員。 它也沒有好名字,因此默認約定不適用。
例如,W / O流利配置EF核心可以正確地創建一個與許多聯想Parent
和Child
的實體,而是因為它不會發現int IChild<Parent>.ForeignKey { get; set; }
int IChild<Parent>.ForeignKey { get; set; }
int IChild<Parent>.ForeignKey { get; set; }
屬性,它將通過ParentOneId
/ ParentTwoId
陰影屬性維護FK屬性值,而不是通過接口顯式屬性。 換句話說,EF Core不會填充這些屬性,也不會被更改跟蹤器考慮。
要讓EF Core使用它們,您需要分別使用HasForeignKey
和HasColumnName
流暢的API方法映射FK屬性和數據庫列名, HasColumnName
重載接受string
屬性名稱。 請注意,字符串屬性名稱必須使用命名空間完全限定。 雖然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查詢)。 使用公共屬性隱式實現interface屬性時沒有這樣的問題。
我們稍后會回到這個限制。 要完成映射,請將以下內容添加到OnModelCreating
覆蓋:
ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);
現在,EF Core將正確加載/考慮您明確實現的FK屬性。
現在又回到了局限。 使用通用對象服務(如AdoptAll
方法或LINQ to Objects)沒有問題。 但是,您無法在用於訪問EF Core元數據或LINQ to Entities查詢內的表達式中一般訪問這些屬性。 在后一種情況下,您應該通過導航屬性訪問它,或者在兩種情況下都應該通過從ChildForeignKeyPropertyName<TParent>()
方法返回的名稱訪問它。 實際上查詢會起作用,但會在本地進行評估,從而導致性能問題或意外行為。
例如
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
外鍵。 它遇到的問題與直接使用約束接口屬性相同,所以我認為它不起作用。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.