Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polymorphic relationship on base class #12867

Closed
Mardoxx opened this issue Aug 2, 2018 · 2 comments
Closed

Polymorphic relationship on base class #12867

Mardoxx opened this issue Aug 2, 2018 · 2 comments

Comments

@Mardoxx
Copy link
Contributor

Mardoxx commented Aug 2, 2018

I want to have a collection of Root objects which have a Details property of some implementation of a common abstract base class.

Something like this.

public class Root<TDetails>
    where TDetails : Details<TDetails>
{
    public int Id { get; set; }
    public TDetails Details { get; set; }
}

public abstract class Details<TSelf>
    where TSelf : Details<TSelf>
{
    public Root<TSelf> Root { get; set; }
}

With ability to do

foreach(var root in db.Roots.Include(x => x.Details))
{
    // do something with root properties

    // some factory method...
    if(root.Details.GetType() == typeof(ADetails))
    {
        aDetailsHandler.Handle(root.Details);
    } else if (root.Details.GetType() == typeof(BDetails))
    {
        bDetailsHandler.Handle(root.Details);
    }
}

Is there a supported way I can do this?

Maybe what I want is something mid way between TPC and table splitting - where the common properties are kept in one table, extra properties per type are stored on separate tables.

What I want to avoid is doing the following:

foreach(var root in db.Roots)
{
    switch(root.DetailsType) {
        case "ADetails":
            var adetails = getADetailsByRootId(root.Id);
            break;

        case "BDetails":
            var bdetails = getBDetailsByRootId(root.Id);
            break;
        default:
            throw new Exception();
    }
}

Perhaps there's a better way to model this data?

@Mardoxx
Copy link
Contributor Author

Mardoxx commented Aug 2, 2018

I came up with this... But now I feel like I should be thrown in programmer jail for library abuse.

public abstract class RootBase
{
    protected RootBase()
    {
    }

    public int Id { get;set; }
    public string Name { get;set; }

    public string DetailsType { get; protected set; }

    [NotMapped]
    public abstract DetailsBase Details { get; set; }
}

public abstract class Root<TDetails> : RootBase
    where TDetails : Details<TDetails>
{
    public Root()
    {
    }

    protected internal virtual TDetails DetailsInternal { get; set; }
    public override DetailsBase Details { get => DetailsInternal; set => DetailsInternal = (TDetails)value; }
}

public abstract DetailsBase
{
    protected DetailsBase()
    {
    }

    public int Id { get; set; }
}

public abstract Details<TSelf> : DetailsBase
    where TSelf : Details<TSelf>
{
    protected DetailsBase()
    {
    }

    public virtual Root<TSelf> Root { get; set; }
}

public class UnCoolDetails : DetailsBase
{
    public const string DetailsType = "uncool";

    public UnCoolDetails()
    {
    }

    public int UnCoolLevel { get; set; }
}

public class CoolDetails : DetailsBase
{
    public const string DetailsType = "cool";

    public CoolDetails()
    {
    }

    public string CoolName { get; set; }
}


public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<RootBase> Roots { get; set; }
    public DbSet<CoolDetails> CoolDetails { get; set; }
    public DbSet<UnCoolDetails> UnCoolDetails { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Configure<RootBase>(b => {
            b.ToTable("Roots");

            b.HasKey(e => e.Id);

            b.HasIndee(e => e.Name)
                .IsUnique();

            b.HasDiscriminator(e => e.DetailsType)
                .HasValue<Person<CoolDetails>>(CoolDetails.DetailsType)
                .HasValue<Person<UnCoolDetails>>(UnCoolDetails.DetailsType);
        });

        modelBuilder.Configure<CoolDetails>(b => {
            b.ToTable("CoolDetails");

            b.HasKey(e => e.Id);

            b.HasOne(e => e.Root)
                .WithOne(e => e.DetailsInternal)
                .HasForeignKey<Root<CoolDetails>>("CoolDetailsId");
        });

        modelBuilder.Configure<UnCoolDetails>(b => {
            b.ToTable("UnCoolDetails");

            b.HasKey(e => e.Id);

            b.HasOne(e => e.Root)
                .WithOne(e => e.DetailsInternal)
                .HasForeignKey<Root<UnCoolDetails>>("UnCoolDetailsId");
        });
    }
}


using(var db = new DbContext())
{
    db.Roots.Add(new Root<CoolDetails>
    {
        Name = "Config_1",
        Details = new CoolDetails
        {
            CoolName = "Super Cool"
        }
    });
    db.SaveChangesAsync();
}

using(var db = new DbContext())
{
    var Roots = db.Roots.Include("DetailsInternal").First(x => x.Name == "Config_1");

    //Roots.First().Details.GetType() == typeof(CoolDetails);
}

Also it throws a null reference exception if you do db.Roots.Include(x => x.Details) and this is the API I want to use 😢

@ajcvickers
Copy link
Member

@Mardoxx It's not clear to me exactly what you want the database mapping to be, or whether you need the generic types, etc. as opposed to them being included just to try to get it to work. Polymorphic relationships are not fully supported (#7623), but some things do work. Here's an example which, from what I can tell, does basically what you are asking for:

public class Root
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual Details Details { get; set; }
}

public abstract class Details
{
    public int Id { get; set; }
}

public class UnCoolDetails : Details
{
    public int UnCoolLevel { get; set; }
}

public class CoolDetails : Details
{
    public string CoolName { get; set; }
}

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 
        => optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");

    public DbSet<Root> Roots { get; set; }
    public DbSet<CoolDetails> CoolDetails { get; set; }
    public DbSet<UnCoolDetails> UnCoolDetails { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<UnCoolDetails>()
            .HasOne<Root>()
            .WithOne(e => (UnCoolDetails)e.Details)
            .HasForeignKey<Root>();

        modelBuilder
            .Entity<CoolDetails>()
            .HasOne<Root>()
            .WithOne(e => (CoolDetails)e.Details)
            .HasForeignKey<Root>();
    }
}

public class Program
{
    public static void Main()
    {
        using (var context = new AppDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.AddRange(
                new Root
                {
                    Details = new UnCoolDetails {UnCoolLevel = 77}
                },
                new Root
                {
                    Details = new CoolDetails {CoolName = "Coolio"}
                });

            context.SaveChanges(false);
        }

        using (var context = new AppDbContext())
        {
            foreach (var root in context.Roots.Include(e => e.Details).ToList())
            {
                Console.WriteLine($"Root {root.Id} has {root.Details.GetType().Name} {root.Details.Id}");
            }
        }
    }
}

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants