Jason N. Gaylord Jason N. Gaylord - 1 month ago 11
C# Question

Using Generics in Interfaces with Circular Reference

Let's assume that I have 4 interfaces that look like the following:

interface IMain
{
ICollection<ISub> Subs { get; set; }
}

interface ISub
{
ICollection<ISubPart> SubParts { get; set; }
}

interface IPart
{
ICollection<ISubPart> SubParts { get; set; }
}

interface ISubPart
{
ISub Sub { get; set; }
IPart Part { get; set; }
}


The end goal is for me to use these interfaces in one class library and implement them with classes in another.

If I try to implement the interfaces, the containing collection or object must still be the interface type rather than the class type. For instance:

public class Sub : ISub
{
ICollection<ISubPart> SubParts { get; set; }
}


I'm using Entity Framework and EF Migrations. When I try to run the migration, it fails with the error:

The entity type 'MyProject.ISubPart' provided for the argument 'clrType' must be a reference type.


To attempt to circumvent this, I was thinking that I could pass the type in as a generic. That's easy to do until I get to ISubPart since ISubPart would have to generics that would have a circular reference back to the parent. So, something like this:

interface ISubPart<TSub, TPart>
where TSub : class
where TPart : class
{
TSub Sub { get; set; }
TPart Part { get; set; }
}


But, if ISub needed a generic type passed in to define ISubPart, then the generic for ISubPart would need to pass in the containing type as well. So, I'd almost need something like the following, which I know doesn't exist:

interface ISubPart<TSub<TSubPart>, TPart>
where TSub : class
where TSubPart : this
where TPart : class
{
TSub<TSubPart> Sub { get; set; }
TPart Part { get; set; }
}


Here's my DBContext:

public abstract class MyDbContext : MyDbContext<Main, Sub, Part>
{
protected MyDbContext() { }

public MyDbContext(DbContextOptions options) : base(options) { }
}

public abstract class MyDbContext<TMain, TSub, TPart> : DbContext
where TMain : Main
where TSub : Sub
where TPart : Part
{
protected MyDbContext() { }
public MyDbContext(DbContextOptions options) : base(options) { }

public DbSet<TMain> Mains { get; set; }
public DbSet<TSub> Subs { get; set; }
public DbSet<TPart> Parts { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<SubPart>()
.HasOne(o => o..Sub)
.WithMany(m => m.SubParts as List<SubPart>)
.HasForeignKey(f => f.SubId);

builder.Entity<SubPart>()
.HasOne(o => o.Part)
.WithMany(m => m.SubParts as List<SubPart>)
.HasForeignKey(f => f.SubId);

base.OnModelCreating(builder);
}
}


I can't be the first person with this issue. Thanks in advance.

Answer

To do this with EF Core you need to cast in the navigation expression:

        builder.Entity<SubPart>()
            .HasOne(o => (Sub)o.Sub)
            .WithMany(m => m.SubParts)
            .HasForeignKey(f => f.Id);

        builder.Entity<SubPart>()
            .HasOne(o => (Part)o.Part)
            .WithMany(m => m.SubParts)
            .HasForeignKey(f => f.Id);