簡體   English   中英

我可以在EF Core中使用帶外鍵的接口,並使用Fluent API將其設置為外鍵嗎?

[英]Can I use an Interface with a Foreign Key in EF Core and set it as a foreign key using Fluent API?

我試圖限制一對夫婦的generic方法,只允許EntitiesinheritIParentOf<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核心可以正確地創建一個與許多聯想ParentChild的實體,而是因為它不會發現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使用它們,您需要分別使用HasForeignKeyHasColumnName流暢的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列以及嘗試將其用作ParentOneParentTwo外鍵。 它遇到的問題與直接使用約束接口屬性相同,所以我認為它不起作用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM