Table per Hierarchy table mapping with both shared and unshared columns

175 views Asked by At

This is an extension of this question which from what I can tell now works in EF6. However, it seems as though there's a problem when you've got subclasses which have both shared and unshared properties.

Let's say this is my model setup:

public abstract class Document
{
    public int Id { get; set; }
    public string NameOnDocument { get; set; }
}

public class BirthCertificate : Document
{
    public string RegistrationNumber { get; set; }
}

public class Licence : Document
{
    public string LicenceNumber { get; set; }
}

In the database, I want BirthCertificate.RegistrationNumber and Licence.LicenceNumber to share the same column, Number. As such, I'm setting up my model like this:

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

    // Document - base class
    modelBuilder.Entity<Document>().HasKey(d => d.Id);
    modelBuilder.Entity<Document>()
        .Property(d => d.Id)
        .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    modelBuilder.Entity<Document>()
        .Property(d => d.NameOnDocument)
        .HasColumnName("Name");

    // Birth certificate
    modelBuilder.Entity<Document>().Map<BirthCertificate>(map => 
        map.Property(c => c.RegistrationNumber).HasColumnName("Number"));

    // Licence
    modelBuilder.Entity<Document>().Map<Licence>(map => 
        map.Property(l => l.LicenceNumber).HasColumnName("Number"));
}

When I generate the database, it all looks and works as expected:

DB Model

Now for the issue at hand. Let's say the Licence entity also needs to record an expiry date; so I add that in as follows:

public class Licence : Document
{
    public string LicenceNumber { get; set; }
    public DateTime ExpiryDate { get; set; }
}

Now when I regenerate the database, it looks like this:

enter image description here

What's more, if I try to insert a licence and a birth certificate, I get the following exception:

An unhandled exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' occurred in EntityFramework.dll

Additional information: A value shared across entities or associations is generated in more than one location. Check that mapping does not split an EntityKey to multiple store-generated columns.

I can understand why that exception is raised - because the database is useless.

What have I missed?

2

There are 2 answers

0
gerrod On BEST ANSWER

Ok, so it turns out that the problem is simple to solve, and yet so far as I can tell is not documented anywhere. So hopefully this will help someone with the same problem.

Seemingly, the key is that you have to map every property on the derived entity:

modelBuilder.Entity<Document>().Map<Licence>(map =>
{
    map.Property(l => l.LicenceNumber).HasColumnName("Number");
    map.Property(l => l.ExpiryDate).HasColumnName("ExpiryDate");
});

Now my database generates as I would expect and all is right with the world.

3
Colin On

I'm not sure you are gaining much by putting 2 columns into 1 in the database. I don't think you'll save much space and you can't interrogate the Number of a base Document, even though both documents will have one. Have you considered adding the Number field to the base and using data annotations on overrides - something like this?

public abstract class Document
{
    public int Id { get; set; }
    public string NameOnDocument { get; set; }
    public virtual string Number
}

public class BirthCertificate : Document
{
    [Display(Name="Registration Number"]
    [Required]
    public override string Number { get; set; }
}

public class Licence : Document
{
    [Display(Name="Licence Number"]
    [Required]
    public override string Number { get; set; }
}

EDIT And if all documents aren't numbered:

public abstract class Document
{
    public int Id { get; set; }
    public string NameOnDocument { get; set; }
}

public abstract class NumberedDocument : Document
{
    public virtual string Number
}

public class BirthCertificate : NumberedDocument
{
    [Display(Name="Registration Number"]
    [Required]
    public override string Number { get; set; }
}