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

c# entity-framework entity-framework-core foreign-keys interface

Question

I am trying to restrict a couple of generic methods to only be allowed Entities that inherit from the IParentOf<TChildEntity> interface, as well as accessing an Entity's Foreign Key (ParentId) Generically.

To demonstrate;

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;
    }
}

A child entity class model would look like this,

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; }
}

First of all, is this possible to do? Or will it breakdown in EF?

Secondly, since the Foreign Keys do not follow convention (and there are multiple) how do I set them via Fluent Api? I can see how to do this in Data Annotations.

I hope this is clear, I have been considering it for a while and trying to work round it, so I can follow my argument, but it may not be clearly conveyed, so please ask for clarification if needed. My reason for wanting to do this is to make the code safe as well as automating a lot of the manual changing of classes necessary to add new associations and entities.

Thanks.

Edit

I decided to create some basic classes to implement this idea and test it, my code is as follows.

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 simply gives each entity an Id property.

I have standard Generic Repositories set up with a Unit of Work class for mediating. The AdoptAll method looks like this in my program.

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;
    }
}

This seems to work correctly and without faults (minimal testing) are there any major flaws in doing this?

Thanks.

Edit Two

This is the OnModelCreating Method in the DbContext, which sets up the foreign key for each entity. Is this problematic?

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
1
7/9/2018 11:13:21 PM

Accepted Answer

According to the updated example, you want to hide the explicit FK from the entity class public interface, and still let it be visible to EF Core and mapped to the FK column in the database.

The first problem is that the explicitly implemented interface member is not directly discoverable by EF. Also it has no good name, so the default conventions don't apply.

For instance, w/o fluent configuration EF Core will correctly create one to many associations between Parent and Child entities, but since it won't discover the int IChild<Parent>.ForeignKey { get; set; } properties, it would maintain the FK property values through ParentOneId / ParentTwoId shadow properties and not through interface explicit properties. In other words, these properties will not be populated by EF Core and also not considered by the change tracker.

To let EF Core use them you need to map both FK property and database column name using respectively HasForeignKey and HasColumnName fluent API method overloads accepting string property name. Note that the string property name must be fully qualified with the namespace. While Type.FullName provides that string for non-generic types, there is no such property/method for generic types like IChild<ParentOne> (the result has to be "Namespace.IChild<Namespace.ParentOne>"), so let first create some helpers for that:

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";

The next would be creating a helper method for performing the necessary configuration:

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);
}

As you can see, I'm using EF Core provided metadata services to find the names of the corresponding navigation properties.

But this generic method actually shows the limitation of this design. The generic constrains allow us to use

childEntity.Property(c => c.ForeignKey)

which compiles fine, but doesn't work at runtime. It's not only for fluent API methods, but basically any generic method involving expression trees (like LINQ to Entities query). There is no such problem when the interface property is implemented implicitly with public property.

We'll return to this limitation later. To complete the mapping, add the following to your OnModelCreating override:

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

And now EF Core will correctly load / take into account your explicitly implemented FK properties.

Now back to limitations. There is no problem to use generic object services like your AdoptAll method or LINQ to Objects. But you can't access these properties generically in expressions used to access EF Core metadata or inside LINQ to Entities queries. In the later case you should access it through navigation property, or in both scenarios you should access in by the name returned from the ChildForeignKeyPropertyName<TParent>() method. Actually queries will work, but will be evaluated locally thus causing performance issues or unexpected behaviors.

E.g.

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);
}

To recap shortly, it's possible, but use with care and make sure it's worth for the limited generic service scenarios it allows. Alternative approaches would not use interfaces, but (combination of) EF Core metadata, reflection or Func<...> / Expression<Func<..>> generic method arguments similar to Queryable extension methods.

Edit: Regarding the second question edit, fluent configuration

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);

produces the following migration for 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);
    });

Note the single ForeignKey column and the attempt to use it as foreign key to both ParentOne and ParentTwo. It suffers the same problems as using a constrained interface property directly, so I would assume it not working.

3
7/10/2018 7:02:44 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow