Reputation: 2144
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
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