I had never used this feature of EF Core before but found it useful today. It seems like something you’d learn when first starting out with an ORM, but I guess I never encountered a use for it before.
Say you have a BaseEntity
and some derived entities:
public abstract class BaseEntity
{
public long Id { get; private init; }
public string Name { get; private set; }
public DateOnly EndDate { get; private set; }
protected BaseEntity(long id, string name, DateOnly endDate)
{
Id = id;
Name = name;
EndDate = endDate;
}
}
public class DerivedEntity1 : BaseEntity
{
public string SharedProperty { get; private set; }
public decimal DecimalProperty { get; private set; }
public DerivedEntity1(long id, string name, DateOnly endDate, string sharedProperty, decimal decimalProperty)
: base(id, name, endDate)
{
SharedProperty = sharedProperty;
DecimalProperty = decimalProperty;
}
}
public class DerivedEntity2 : BaseEntity
{
public string SharedProperty { get; private set; }
public long LongProperty { get; private set; }
public DerivedEntity2(long id, string name, DateOnly endDate, string sharedProperty, long longProperty)
: base(id, name, endDate)
{
SharedProperty = sharedProperty;
LongProperty = longProperty;
}
}
public class DerivedEntity3 : BaseEntity
{
public DerivedEntity3(long id, string name, DateOnly endDate) : base(id, name, endDate)
{
}
}
In EF Core, what are your options for storing these? According to the .NET docs, you can:
- Store them in the same table
- Each entity gets their own table, but only if it has different properties
- Each entity gets their own table, period
In my case, I had 10…yes, TEN of them, so I definitely did not want to have separate tables. But storing in the same table, which they call table-per-hierarchy (TPH), means the table must have a column for every property that is unique to a derived entity.
So in the case above, you would need a table with columns for properties in DerivedEntity1
, DerivedEntity2
, and DerivedEntity3
, along with the inherited properties from BaseEntity
:
create table entities
(
"Id" bigint generated by default as identity
constraint "PK_entities"
primary key,
name text not null,
end_date date not null,
type varchar(21) not null,
shared_prop text,
decimal_prop numeric,
long_prop bigint
);
I didn’t really like the idea of columns that were “unused” for certain rows but I only needed 5 additional columns. These columns, or any future ones, were purely for display purposes and did not need indexes, so this worked for me.
To configure these in Entity Framework Core, you need to define a discriminator
, which is a column that is stored on the table that determines the underlying type of the row.
public sealed class BaseEntityTypeConfiguration : IEntityTypeConfiguration<BaseEntity>
{
public void Configure(EntityTypeBuilder<BaseEntity> builder)
{
builder.ToTable("entities");
builder.HasDiscriminator<string>("type")
.HasValue<DerivedEntity1>("derivedType1")
.HasValue<DerivedEntity2>("derivedType2")
.HasValue<DerivedEntity3>("derivedType3");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name)
.HasColumnName("name")
.IsRequired();
builder.Property(e => e.EndDate)
.HasColumnName("end_date")
.IsRequired();
}
}
builder.HasDiscriminator<string>("type")
creates a type
column when running Migrations
that stores its value as the corresponding entity type:
.HasValue<DerivedEntity1>("derivedType1")
.HasValue<DerivedEntity2>("derivedType2")
.HasValue<DerivedEntity3>("derivedType3");
Since the BaseEntityTypeConfiguration
already defined some columns above, when you configure the derived entities, you just need to define their unique columns:
public sealed class DerivedEntity1TypeConfiguration : IEntityTypeConfiguration<DerivedEntity1>
{
public void Configure(EntityTypeBuilder<DerivedEntity1> builder)
{
builder.Property(e => e.SharedProperty)
.HasColumnName("shared_prop")
.IsRequired();
builder.Property(e => e.DecimalProperty)
.HasColumnName("decimal_prop")
.IsRequired();
}
}
public sealed class DerivedEntity2TypeConfiguration : IEntityTypeConfiguration<DerivedEntity2>
{
public void Configure(EntityTypeBuilder<DerivedEntity2> builder)
{
builder.Property(e => e.SharedProperty)
.HasColumnName("shared_prop")
.IsRequired();
builder.Property(e => e.LongProperty)
.HasColumnName("long_prop")
.IsRequired();
}
}
And since DerivedEntity3
has no unique columns, you don’t need to write anything extra. All you need to do is add these configs to your DbContext
and Migrations
will handle setting up the table.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new BaseEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new DerivedEntity1TypeConfiguration());
modelBuilder.ApplyConfiguration(new DerivedEntity2TypeConfiguration());
}
The resulting table with data might look like:
Id | name | end_date | type | shared_prop | decimal_prop | long_prop |
---|---|---|---|---|---|---|
1 | entity1 | 2024-07-02 | derivedType1 | shared1 | 1.23 | null |
2 | entity2 | 2024-06-04 | derivedType2 | shared2 | null | 123 |
3 | entity3 | 2024-05-12 | derivedType3 | null | null | null |