I am implementing an audit functionality to keep track of any changes (create, update, delete) made to any type of object. For this I need a way to declare a generic object which can point to object of any other class derived from an abstract base class.
Base class I do not want to have table for in database :
public abstract class EntityBase {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DataType(DataType.Date)]
public DateTime CreationDateTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[DataType(DataType.Date)]
public DateTime ModificationDateTime { get; set; }
}
public class EntityBaseConfigurations<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : EntityBase {
public virtual void Configure(EntityTypeBuilder<TEntity> builder) {
builder.Property(e => e.CreationDateTime).IsRequired().HasDefaultValueSql("GETUTCDATE()");
builder.Property(e => e.ModificationDateTime).IsRequired().HasDefaultValueSql("GETUTCDATE()");
}
}
and a couple of models derived from base for example :
[Table("Initiative")]
public class Initiative : EntityBase {
[Key]
public int InitiativeId { get; set; }
// ...
[Required]
public Contributor Assignee { get; set; }
}
public class InitiativeConfiguration : EntityBaseConfigurations<Initiative> {
public override void Configure(EntityTypeBuilder<Initiative> builder) => base.Configure(builder);
// ...
}
[Table("Contributor")]
public class Contributor : EntityBase {
[Key]
public int ContributorId { get; set; }
// ...
}
public class ContributorConfiguration : EntityBaseConfigurations<Contributor> {
public override void Configure(EntityTypeBuilder<Contributor> builder) => base.Configure(builder);
// ...
}
Now my failed attempt to create the Audit model
[Table("Audit")]
public class Audit : EntityBase {
[Key]
public int AuditId { get; set; }
public User Actor {get; set;}
// ...
public EntityBase Entity { get; set; } // I want to be able to point to objct of any class derived from EntityBase (ex. Initiative, Contributor)
}
public class AuditConfiguration : EntityBaseConfigurations<Audit> {
public override void Configure(EntityTypeBuilder<Audit> builder) => base.Configure(builder);
// ...
}
When I try to create a migration for audit class, I get below error
The derived type 'Audit' cannot have KeyAttribute on property 'AuditId' since primary key can only be declared on the root type.
Here is my Db Context if needed
public class DbContext : IdentityDbContext<User> {
public DbContext(DbContextOptions<DbContext> options) : base(options) { }
public DbSet<Initiative> Initiatives { get; set; }
public DbSet<Contributor> Contributors { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new InitiativeConfiguration());
modelBuilder.ApplyConfiguration(new ContributorConfiguration());
}
}
CodePudding user response:
try to remove [DatabaseGenerated(DatabaseGeneratedOption.Identity)] attribute from public DateTime CreationDateTime propert of EntityBase.
Also I would remove
public EntityBase Entity { get; set; }
from Audit class , you would not be able to use it. Or you can try to make it not mapped
[NotMapped]
public EntityBase Entity { get; set; }
CodePudding user response:
By this Entity
property in Audit
, EF takes a completely different path in implementing the model.
Without it, it maps each entity to its own independent table, each table having all columns including the ones in
EntityBase
. In fact, it completely ignoresEntityBase
. As for EF, there is no inheritance.If it's there, EF recognizes that
Audit
needs a foreign key to any type derived fromEntityBase
. In order to implement this polymorphic association, a base table is required that "collects" all primary key values ofEntityBase
entities.Audit
refers to this base table's primary key.Also, table
EntityBase
contains all shared properties (CreationDateTime
etc), and the separate entity table only have their own properties and a primary key that's also a foreign key toEntityBase
. This is referred to as Table per Type (TPT) inheritance.
This TPT inheritance also requires EntityBase
to have the primary key property, implying that the inheriting entities shouldn't.
All in all, if you remove the key properties from the inheriting entities and add
public int ID { get; set; }
to EntityBase
, EF will be able to map your class model.
However, be aware of the ins and outs of TPT inheritance, esp. the warning
In many cases, TPT shows inferior performance when compared to TPH.
And, of course, more so compared to no inheritance.