我可以在EF Core中使用带有外键的接口,并使用Fluent API将其设置为外键吗?

时间:2018-07-06 13:17:51

标签: c# entity-framework interface foreign-keys entity-framework-core

我正在尝试限制仅从generic Entitiesinherit的{​​{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;
    }
}

1 个答案:

答案 0 :(得分:3)

根据更新后的示例,您想从实体类公共接口隐藏显式FK,并且仍然让其对于EF Core可见并映射到数据库中的FK列。

第一个问题是,EF无法直接发现显式实现的接口成员。而且它没有好名字,因此默认约定不适用。

例如,没有流畅的配置EF Core将在ParentChild实体之间正确创建一对多关联,但是由于它不会发现int IChild<Parent>.ForeignKey { get; set; }属性,因此它可以将通过ParentOneId / ParentTwoId shadow properties而不是通过接口显式属性来维护FK属性值。换句话说,这些属性将不会被EF Core填充,也不会被变更跟踪器考虑。

要让EF Core使用它们,您需要分别使用HasForeignKeyHasColumnName流利的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列,并尝试将其用作ParentOneParentTwo的外键。与直接使用受约束的接口属性一样,它也会遇到同样的问题,因此我认为它不起作用。