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

Tristan Trainer

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

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.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

EF Foreign Key using Fluent API

In EF Core, how do I configure the foreign key for reference owned types using Fluent API

How to change foreign key name in EF Core Fluent API?

EntityFramework foreign key as primary key with fluent API

EF Core: Using ID as Primary key and foreign key at same time

EF core 2 composite primary key using Id and a foreign key

Foreign key with composite key in EF Core

How to assign default value to a foreign key using fluent api?

How to set foreign key where column names are different using Entity Framework 5 Fluent API?

Expose Foreign key through fluent API

Foreign Key with Fluent API - Code First

How to configure a composite primary key containing a foreign key (EF Fluent API)

How do I add a foreign key to Identity user in EF Core?

Ef Core foreign key unique which is not what I want

How to insert data into multiple tables with a foreign key using EF core?

Can't retrieve EF Foreign Key values in WebAPI, DTO, AutoMapper and .NET Core using VS Code

How can I change foreign key option on MySQL EF 6?

Add Foreign Key Column In EF Core

Prevent EF Core from generating foreign key

EF Core Not Updating Foreign Key Items Correctly

EF Core: Foreign key returns null value

EF Core - many to many with another foreign key

EF Core handle shadow foreign key DeleteBehaviour

EF Core Foreign Key configuration problem

EF Core creates multiple foreign key columns

Composite Key EF Core getting error when using Fluent Api

EF foreign key is NULL

Fluent Nhibernate error Foreign Key

Entity Framework Core fluent api One-To-Many and One-To-One produces duplicate foreign key