Harald Coppoolse Harald Coppoolse - 21 days ago 8
C# Question

Entity Framework: TPC MapInheritedProperties model super class properties

Summary: in Entity Framework I use TPC to create two classes derived from the same base class. In fluent API I map inherited properties, but how to model the properties of the base class?

More extensive description
In Entity Framework I have a class Child, and two kinds of Children: a Boy and a Girl. Both Boy and Girl derive from Child:

public class Child
{
public int Id {get; set;}
public string Name {get; set;}
}
public class Boy : Child
{
public string SomeBoyishProperty {get; set;}
}
public class Girl : Child
{
public string SomeGirlyProperty {get; set;}
}


I want a table with boys and a table with girls, each table also having the Child properties.

public class MyDbContext : DbContext
{
public DbSet<Boy> Boys {get; set;}
public DbSet<Girl> Girls {get; set;
}


From several sources, for example this one I learned that this is called TPC: table per concrete class and that I should MapInheritedProperties in OnModelCreating

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

// model the properties of the base class, for instance set max length
modelBuilder.Entity<Child>()
.Property(p => p.Name).IsRequired().HasMaxLength(12);

// Model Daughter:
modelBuilder.Entity<Daughter>()
.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Daughters");
})
.Property(p => p.SomeGirlyProperty).IsOptional().HasMaxLength(13);

// model Boy
modelBuilder.Entity<Son>()
.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Sons");
})
.Property(p => p.SomeBoyishProperty).IsOptional().HasMaxLength(14);
}


During SaveChanges I get an InvlidOperationException indicating that the primary key is not unique. Removing the part that builds Child solves this problem.

How to build the Child properties without having to do this in the Girl and again in the Boy properties?

Answer

SHORT ANSWER:

If you want your code to work, remove any reference to Child entity in your model configuration. As soon as EF knows about Child as Entity it will enforce the following rule: There cannot be 2 entities of type Child or 2 entities that inherit from Child with the same PK in memory. You can see the error tells you that the entities where successfully persisted; but when EF pulls the new IDs it finds out both have the same ID.

LONG ANSWER

Remove

modelBuilder.Entity<Child>()
    .Property(p => p.Name).IsRequired().HasMaxLength(12);

Instead this is how your OnModelCreating method should look like.

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

    // Model Daughter:
    var girlEntity = modelBuilder.Entity<Girl>();
    girlEntity.Map(m =>
    {
        m.MapInheritedProperties();
        m.ToTable("Daughters");
    });
    girlEntity.Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    girlEntity.Property(p => p.Name).IsRequired().HasMaxLength(12);
    girlEntity.Property(p => p.SomeGirlyProperty).IsOptional().HasMaxLength(13);

    // model Boy
    var boyEntity = modelBuilder.Entity<Boy>();
    boyEntity.Map(m =>
    {
        m.MapInheritedProperties();
        m.ToTable("Sons");
    });
    boyEntity.Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    boyEntity.Property(p => p.Name).IsRequired().HasMaxLength(12);
    boyEntity.Property(p => p.SomeBoyishProperty).IsOptional().HasMaxLength(14);
}

If you don't want the configuration repetition in the configuration I would use the DataAnnotations attributes on the base class to enforce the name to be required.

You will also need to enforce that the Id property to be auto-generated in database. This doesn't happen by convention when using the Map method in the fluent API. You can see I added the fluent calls to make that happen in both the Girl and Boy mapping.

Hope this helps.