0

I am trying to achieve the following relationship in my model (simplified). In other words, each Employee can have multiple bank Accounts (private account, corporate account, etc...). Each employee can bring any company related Receipt (e.g. gas bill, police fine, etc). Now, depending on the total value of this receipt company can decide whether they will charge this value immediately to user, or, in case this is large amount of money, they need to deduct it monthly (thus each receipt can have multiple Transactions which are in this context understood as deductions). If Receipt is charged immediately (see ReceiptType property on a model), then only one Transaction exists for this entity. Finally (especially in case of long term deductions), Transaction needs to have access to Account as we need to specify, to which employee's account this value will be charged.

enter image description here

Here is how I specified entities:

public class EmployeeDB
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
    public virtual IEnumerable<EmployeeAccountDB> Accounts { get; set; }
    public virtual IEnumerable<ReceiptDB> Receipts { get; set; }
}

public class EmployeeAccountDB
{
    public long Id { get; set; }
    public long EmployeeId { get; set; }
    public virtual EmployeeDB Employee { get; set; }
    public virtual IEnumerable<TransactionDB> Transactions { get; set; }
}

public class ReceiptDB
{
    public long Id { get; set; }
    public long EmployeeDBId { get; set; }
    public ReceiptTypeEnum ReceiptType { get; set; }
    public double Value { get; set; }
    public double VAT { get; set; }
    public double TotalValue { get; set; }

    public virtual EmployeeDB Employee { get; set; }
    public virtual IEnumerable<TransactionDB> Transactions { get; set; }
}

public class TransactionDB
{
    public long Id { get; set; }
    public long EmployeeAccountId { get; set; }
    public long ReceiptDBId { get; set; }

    public virtual ReceiptDB Receipt { get; set; }
    public virtual EmployeeAccountDB Account { get; set; }
}

Now via Fluent API I specified the following Entity Configurations:

public class EmployeeDBConfiguration : IEntityTypeConfiguration<EmployeeDB>
{
    public void Configure(EntityTypeBuilder<EmployeeDB> builder)
    {
        builder.ToTable("Employees", ApplicationDbContext.DefaultSchema);
        builder.HasKey(t => t.Id);
        builder.Property(t => t.HireDate).IsRequired();
        builder.HasMany(x => x.Accounts).WithOne(x => x.Employee).OnDelete(DeleteBehavior.Cascade);
        builder.HasMany(x => x.Receipts).WithOne(x => x.Employee).OnDelete(DeleteBehavior.Cascade);
    }
}

public class EmployeeAccountDBConfiguration : IEntityTypeConfiguration<EmployeeAccountDB>
{
    public void Configure(EntityTypeBuilder<EmployeeAccountDB> builder)
    {
        builder.ToTable("EmployeeAccounts", ApplicationDbContext.DefaultSchema);
        builder.HasKey(t => t.Id);
        builder.Property(t => t.Id).ValueGeneratedOnAdd();
        builder.HasOne(x => x.Employee).WithMany(o => o.Accounts).HasForeignKey(nameof(EmployeeAccountDB.EmployeeId));
        builder.HasMany(x => x.Transactions).WithOne(x => x.Account).OnDelete(DeleteBehavior.Cascade);
    }
}

   public class ReceiptDBConfiguration : IEntityTypeConfiguration<ReceiptDB>
    {
    public void Configure(EntityTypeBuilder<ReceiptDB> builder)
    {
        builder.ToTable("Receipts", ApplicationDbContext.DefaultSchema);
        builder.HasKey(t => t.Id);
        builder.Property(t => t.Id).ValueGeneratedOnAdd();
        builder.Property(t => t.TotalValue).IsRequired();
        builder.HasOne(x => x.Employee).WithMany(x=>x.Receipts).HasForeignKey(x => x.EmployeeDBId);
        builder.HasMany(x => x.Transactions).WithOne(x => x.Receipt).OnDelete(DeleteBehavior.Cascade);
    }
    }

class TransactionDBConfiguration : IEntityTypeConfiguration<TransactionDB>
{
    public void Configure(EntityTypeBuilder<TransactionDB> builder)
    {
        builder.ToTable("Transactions", ApplicationDbContext.DefaultSchema);
        builder.HasKey(t => t.Id);
        builder.Property(t => t.Id).ValueGeneratedOnAdd();
    }
}

Migrations are created just fine. The problem however occurs, when I try to create database via dotnet ef database update. I get the following problem

Introducing FOREIGN KEY constraint 'FK_Transactions_Receipts_ReceiptDBId' on table 'Transactions' may cause cycles or multiple cascade path

I am not really sure what the actual problem is. My goal is (what I tried to set up) when Employee is removed, then all subsequent entities Account, Receipt, Transaction that are related to this entity will be removed as well (as specified in configuration > Cascade delete).

Any help in respect to this matter would be highly appreciated. If my model structure is incorrect, I am more than happy to refactor it in order to keep it simple and clean.

P.S. I am connecting to SQL database.

Robert J.
  • 2,631
  • 8
  • 32
  • 59
  • 1
    Take a look at https://stackoverflow.com/questions/27792131/entity-framework-foreign-key-constraint-may-cause-cycles-or-multiple-cascade-pa . SQL Server cannot handle a chain of deletes. A way out is use logical deleting setting up a `public bool IsActive {get;set;}` in each class – Andre.Santarosa Jan 03 '21 at 19:56
  • This may be a duplicate of: https://stackoverflow.com/questions/851625/foreign-key-constraint-may-cause-cycles-or-multiple-cascade-paths – S. Walker Jan 03 '21 at 19:58
  • Is a Receipt only ever charged to one account? Why does a receipt link to an Employee if it is associated with an account (or transaction which is associated with an account) which is associated with an employee? – Caius Jard Jan 03 '21 at 20:17
  • Receipt is not linked to Account as Transaction is responsible for deducting money from an Account. Receipt is just a holder of information about specific bill or invoice. However Receipt should hold information about Employee in case we need to display all Receipts associated to Employee – Robert J. Jan 03 '21 at 20:24
  • So receipts and transactions are not related? – Caius Jard Jan 03 '21 at 23:05
  • Receipt has information about all the transactions related to repayment of the specific receipt (1-N relationship) – Robert J. Jan 04 '21 at 07:17

1 Answers1

0

I have received quite a few great articles on SO which address exactly my problem, but in the end how I decided to solve it was like following:

For Account I decided to implement soft delete functionality (which causes that Account will never get removed, unless Employee is deleted) and therefore I was able to discard the Employee > Account > Transaction path as following (last line of code):

public class EmployeeAccountDBConfiguration : IEntityTypeConfiguration<EmployeeAccountDB>
{
    public void Configure(EntityTypeBuilder<EmployeeAccountDB> builder)
    {
        builder.ToTable("EmployeeAccounts", ApplicationDbContext.DefaultSchema);
        builder.HasKey(t => t.Id);
        builder.Property(t => t.Id).ValueGeneratedOnAdd();
        builder.HasOne(x => x.Employee).WithMany(o => o.Accounts).HasForeignKey(nameof(EmployeeAccountDB.EmployeeId));
        builder.HasMany(x => x.Transactions).WithOne(x => x.Account).OnDelete(DeleteBehavior.NoAction);
    }
}

This way SQL server sees only 1 proper cascade path within the model (Employee > Receipt > Transaction) which works great.

Robert J.
  • 2,631
  • 8
  • 32
  • 59