1

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());
    }
  }
2
  • 1
    Hi @tzman, the error message is very clear that you cannot define [Key] on the derived type. Commented Dec 28, 2021 at 6:44
  • @Rena I find the error message quite misleading because, if I remove "public EntityBase Entity { get; set; }" line, everything runs just fine. Also I have keys set in other classes (Initiative and Contributor) and they are working as expected. Commented Dec 28, 2021 at 13:18

2 Answers 2

1

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 ignores EntityBase. As for EF, there is no inheritance.

  • If it's there, EF recognizes that Audit needs a foreign key to any type derived from EntityBase. In order to implement this polymorphic association, a base table is required that "collects" all primary key values of EntityBase 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 to EntityBase. 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.

Sign up to request clarification or add additional context in comments.

3 Comments

Is there any better way this audit functionality can be implemented or should I go with TPT inheritance? Basically I need a common place to manage audit information for all kind of operations.
That's a broad subject with many options. Many do it by overriding SaveChanges. Just one example. I wouldn't use TPT inheritance for it though, because it has considerable impact on the class model, only for a cross-cutting concern.
Of course I mean database model, not class model.
0

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; }  

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.