Entity Framework Core Table-per-Hierarchy: Prevent "unregistered classes" from being added as base type

401 Views Asked by At

I need to implement a simple credit system for a web app. So users shall be able to receive credits e.G. by simply paying for them or receive them as a bonus for doing something (like registering, watching an add, etc). On the other hand, the user shall be able to spend these credits for services the web app provides (like promoting their content, enabling VIP status, etc.).

We are building this web app with ASP.net Core 3.1 and Entity Framework Core. So i was thinking about using a TransactionLog in our PostgreSQL database.

But to make it "more fancy" I would like to have these different types of transaction as real types in our model. And that's where I thought table-per-hierarchy comes into play.

We have configured TpH via Fluent API:

// DbSet Properties
public DbSet<AbstractTransaction> CreditTransactions { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder) {
// Inside OnModelCreating Method:
modelBuilder.Entity<AbstractTransaction>()
    .ToTable("CreditTransactions")
    .HasDiscriminator<string>("TransactionType")
    .HasValue<RegisterBonus>("RegisterBonus")
    .HasValue<SinglePayment>("SinglePayment")
    .HasValue<AcivateVIP>("ActivateVIP")
    .HasValue<PostPromoted>("PostPromoted");
}

The class diagram basically looks like this:

Class Diagram

So far this works nice and easy. But I discovered one drawback: when adding a new transaction type (like NewDebitTransaction) to the class hierarchy without adding the corresponding .HasValue<NewDebitTransaction>("NewDebitTransaction") in the DbContext, EF Core still allows me to add object of type NewDebitTransaction to the Transaction-Table, but stores this entry as an AbstractTransaction type. I was hoping to get some exception telling me, that NewDebitTransaction is not a registered type for the TpH-Mapping or something. Instead EF Core silently downcasts my concrete class to an AbstractTransaction and i can never get it back.

The MSDN-Documentation states, that one can forbid types from the hierarchy, by using .HasBaseType((Type)null) but i don't get how to include this in my scenario.

Currently this is a show stopper for me. Because in the future there will be a lot of new transaction types. And it is only a matter of time until someone forgets to add the corresponding .HasValue() call in the DbContext and we silently store AbstractTransactions in the Database, without knowing what kind of transaction it originally was.

Anybody got an idea on how to prevent EF Core from downcasting my Types to a matching Abstract-Type? Or is TpH the wrong tool for that problem?

1

There are 1 best solutions below

0
Guru Stron On

I have not found solution for making EF to do this, but you can work around the issue. One option is to use check constraint to prevent database from inserting such values. Something like this:

modelBuilder.Entity<AbstractTransaction>()
    .HasCheckConstraint("CK_NOT_AbstractTransaction", "TransactionType != \"AbstractTransaction\"")
    ...// rest of set up.

Also I would add a unit test (so this kind of errors would be spotted earlier) which will scan assembly for all of AbstractTransaction's inheritors, create an instance of it, save it to database (inmemory one should work), fetch it and check that TransactionType is not "AbstractTransaction". Though this will require to expose the TransactionType property.