Jamie Twells
Jamie Twells

Reputation: 2144

How can I map a derived class having a collection of entities with Entity Framework

How can I map Dogs and Cats to have a collection of Toys but not Rats because they're not pets?

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

public class Cat : Animal {
    public virtual ICollection<Toy> Toys { get; set; }
}

public class Dog : Animal {
    public virtual ICollection<Toy> Toys { get; set; }
}

public class Rat : Animal {

}

public class Toy {
    public int Id { get; set; }

    public string Name { get; set; }

    public Animal Owner { get; set; }

    public int OwnerId { get; set; }
}

I tried, with fluent mapping, doing:

public class CatEntityConfiguration : EntityConfiguration<Cat> {
    public CatEntityConfiguration() {
        HasMany(c => c.Toys).WithRequired(t => t.Owner);
    }
}

But Owner has to be of type Cat, not Animal, but then of course, Dog wants Owner to be of type Dog, not Animal or Cat. I could make a Dog and a Cat property on Toy, but that seems like a hack, not a solution.

Upvotes: 2

Views: 287

Answers (1)

Der Kommissar
Der Kommissar

Reputation: 5953

The problem with Entity Framework is that it want's a very tight relationship between entities, it really does. If you say 'Toy: you're allowed to have any Animal as an owner, but only some Animal types will have you as a child' then Entity Framework reads that as 'I can't allow Toy to have a reference to Animal implicitly, because Animal doesn't know anything about Toy, and Toy is actually related to a Cat or a Dog.'

The easiest way to fix this is to make Pet a class with Toys, then make Cat and Dog inherit the Pet class, where Rat is still an Animal:

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

public abstract class Pet : Animal
{
    public virtual ICollection<Toy> Toys { get; set; }
}

public class Cat : Pet
{
    public string Name { get; set; }
}

public class Dog : Pet
{
    public int CollarColor { get; set; }
}

public class Rat : Animal
{

}

public class Toy
{
    public int Id { get; set; }
    public Pet Owner { get; set; }
}

EF will then create a Pets table with a Discriminator column and the properties of both our objects (Dog and Cat are no longer the database entities):

CREATE TABLE [dbo].[Pets] (
    [Id]            INT            IDENTITY (1, 1) NOT NULL,
    [Discriminator] NVARCHAR (128) NOT NULL,
    [Name]          NVARCHAR (MAX) NULL,
    [CollarColor]   INT            NULL,
    CONSTRAINT [PK_dbo.Pets] PRIMARY KEY CLUSTERED ([Id] ASC)
);

CreateTable(
    "dbo.Pets",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Discriminator = c.String(nullable: false, maxLength: 128),
            Name = c.String(),
            CollarColor = c.Int(),
        })
    .PrimaryKey(t => t.Id);

And when we add a Cat named Frank, we get:

Id  Discriminator   Name    CollarColor
1   Cat             Frank   NULL

If you don't like this DB structure, the other option is to create a PetAttributes class, of which each Pet gets one, and then Toys is part of that class, each toy is owned by a PetAttribute, and then life goes on. The only problem is your navigation becomes Cat.Attributes.Toys, but you could build a get-only property to get around that.

To get rid of that Discriminator column, we could add:

modelBuilder.Ignore<Pet>();

Then this build a new, but still non-optimal DB structure:

CreateTable(
    "dbo.Toys",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Cat_Id = c.Int(),
            Dog_Id = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.Cats", t => t.Cat_Id)
    .ForeignKey("dbo.Dogs", t => t.Dog_Id)
    .Index(t => t.Cat_Id)
    .Index(t => t.Dog_Id);

So, the last and final option, create a PetAttributes class with the Toys:

public abstract class Pet : Animal
{
    public PetAttributes Attributes { get; set; }
}

public class PetAttributes
{
    [Key]
    public int OwnerId { get; set; }

    [ForeignKey(nameof(OwnerId))]
    public Pet Owner { get; set; }

    public virtual ICollection<Toy> Toys { get; set; }
}

public class Toy
{
    public int Id { get; set; }
    public PetAttributes Owner { get; set; }
}

We override OnModelCreating:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Ignore<Pet>();
    modelBuilder.Entity<Pet>().HasRequired(p => p.Attributes).WithRequiredDependent(a => a.Owner);
}

And we get a new table structure:

CreateTable(
    "dbo.Cats",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Name = c.String(),
            Attributes_OwnerId = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.PetAttributes", t => t.Attributes_OwnerId)
    .Index(t => t.Attributes_OwnerId);

CreateTable(
    "dbo.PetAttributes",
    c => new
        {
            OwnerId = c.Int(nullable: false, identity: true),
        })
    .PrimaryKey(t => t.OwnerId);

CreateTable(
    "dbo.Toys",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Owner_OwnerId = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.PetAttributes", t => t.Owner_OwnerId)
    .Index(t => t.Owner_OwnerId);

Then, we can move more attributes into Pet or PetAttributes, and create get-only properties for them if they are in PetAttributes:

public abstract class Pet : Animal
{
    public string Name { get; set; }

    public PetAttributes Attributes { get; set; }

    public ICollection<Toy> Toys => Attributes.Toys;
}
CreateTable(
    "dbo.Cats",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Name = c.String(),
            Attributes_OwnerId = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.PetAttributes", t => t.Attributes_OwnerId)
    .Index(t => t.Attributes_OwnerId);

CreateTable(
    "dbo.Dogs",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            CollarColor = c.Int(nullable: false),
            Name = c.String(),
            Attributes_OwnerId = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.PetAttributes", t => t.Attributes_OwnerId)
    .Index(t => t.Attributes_OwnerId);

As we can see, EF makes it difficult but not impossible to map one entity as the many part of multiple other entity relationships. This is largely due to the type constraints: EF also makes it hard to make a mistake in relationships from type errors.

Upvotes: 5

Related Questions