代码之家  ›  专栏  ›  技术社区  ›  Tristan Trainer Ferguson Osas

我可以在ef core中使用外键接口并使用fluent api将其设置为外键吗?

  •  1
  • Tristan Trainer Ferguson Osas  · 技术社区  · 6 年前

    我想限制一些 generic 只允许使用的方法 Entities 那个 inherit IParentOf<TChildEntity> interface ,以及访问 Entity's Foreign Key (父ID) 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设置它们?我可以在数据注释中看到如何做到这一点。

    我希望这是清楚的,我已经考虑了一段时间,并试图解决它,所以我可以遵循我的论点,但它可能不清楚传达,所以请要求澄清,如果需要的话。我想这样做的原因是为了确保代码的安全性,并自动执行添加新关联和实体所需的大量手动类更改。

    谢谢。

    编辑

    我决定创建一些基本类来实现这个想法并测试它,我的代码如下。

    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);
    }
    
    1 回复  |  直到 6 年前
        1
  •  3
  •   Ivan Stoev    6 年前

    根据更新的示例,您希望在实体类公共接口中隐藏显式fk,并且仍然让它对ef core可见并映射到数据库中的fk列。

    第一个问题是显式实现的接口成员不能被ef直接发现。而且它没有好名字,所以默认的约定不适用。

    例如,w/o fluent configuration ef core将在 Parent Child 实体,但因为它不会发现 int IChild<Parent>.ForeignKey { get; set; } 属性,它将通过 ParentOneId / ParentTwoId shadow properties 而不是通过接口显式属性。换言之,这些属性不会由ef core填充,也不会被变更跟踪程序考虑。

    要让ef core使用它们,您需要分别使用 HasForeignKey HasColumnName fluent api方法重载接受 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)
    

    它编译得很好,但在运行时不起作用。它不仅适用于fluent api方法,而且基本上适用于任何涉及表达式树的泛型方法(比如linq to entities查询)。如果使用公共属性隐式实现接口属性,则不存在此类问题。

    我们稍后再讨论这个限制。要完成映射,请将以下内容添加到 OnModelCreating 覆盖:

    ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
    ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);
    

    现在ef core将正确地加载/考虑显式实现的fk属性。

    现在回到限制。使用类似于 AdoptAll 方法或对象的LINQ。但不能在用于访问ef核心元数据的表达式中或在linq到实体的查询中通用地访问这些属性。在后一种情况下,您应该通过导航属性访问它,或者在这两种情况下,您都应该通过从 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核心元数据、反射或 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 是的。它遇到的问题与直接使用受约束的接口属性相同,因此我假设它不起作用。