'How did EFCore lose my "undeletable" data, and how can I re-attach it to the database?

I am using net6.0 (C# 10.0) with Entity Framework 6.0.1.

I have a 1:1 relationship between an Parent and Child models. Each share the same primary key (standard split-table design):

    modelBuilder.Entity<Parent>()
      .HasOne(fb => fb.Child)
      .WithOne(i => i.Parent)
      .HasForeignKey<Child>(s => s.Id)
      .OnDelete(DeleteBehavior.Cascade);

Neither model should ever be deletable in practice (on a production server). As a way of guarding against this, I have implemented my DbContext to throw an exception if the relationship is severed, or either of the models is deleted:


  private static readonly List<Type> DeletePreventedModels = new List<Type>() {
    typeof(Parent), typeof(Child)
  };

  private void UpdateChanges() {
    // Prevent deletion of any protected models
    foreach (EntityEntry entry in ChangeTracker.Entries()) {
      if (entry.State != EntityState.Deleted) continue;
      Type t = entry.Entity.GetType();
      if (DeletePreventedModels.Any(mt => mt.IsAssignableFrom(t))) {
        var id = (entry.Entity is IStringKeyData d) ? d.Id : null;
        throw new ArgumentException($"{t.Name} {id} cannot be deleted");
      }
      if (t.IsAssignableTo(typeof(Parent)) && entry.State != EntityState.Unchanged) {
        Parent fb = (Parent) entry.Entity;

        if (fb.Child == null) throw new NullReferenceException($"Parent {fb.Id} had null child");
      }
    }
  }

  public override int SaveChanges() {
    UpdateChanges();
    return base.SaveChanges();
  }

  public override Task<int> SaveChangesAsync(
    bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new()
  ) {
    UpdateChanges();
    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
  }

I have taken this heavy-handed approach because I have been losing Child data. Specifically, I am finding rows missing from the MySQL table. There is nothing else which edits (or even accesses) the table, and I cannot figure out where these deletions are coming from. I know the data was originally present based upon database backups.

In attempts to correct the problem, I have added some code to the Parent class which is run after the model is loaded from the database (which does .Include(p => p.Child) btw).

  internal async Task FixChild(MyDataContext context) {

    if (Child == null) {
      this.Child = new Child() { Id = this.Id, Parent = this };
      if (context.Entry(Child).State == EntityState.Detached) {
        context.Attach(Child);
      }

      await Child.RestoreFromBackup(context);

      context.Log.Warning("[UPDATE] EMPTY CHILD {state} {id}",
        context.Data.Entry(Child).State, Id);

      await context.SaveChangesAsync();
    }
 }

When this code runs, the broken children are successfully detected and restored from the backup. The child starts in the detached state, and then shows the Modified state (based on the log statement) once the RestoreFromBackup function runs. However, when we hit the SaveChanges line, I get:

The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

There are no other warnings/errors in the logs. The restored child data seems complete, in that I can log the restored object and it matches the database backup (the database structure has not been changed since the backup).

In summary:

  • How do I prevent the Child data from being lost?
  • How do I properly save the restored Child data?


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source