Duplicate entries for derived navigation properties in EF Core 6

Viewed 302

I have an .NET 6 API server (currently) using a local instance of SQL Server. All non-Image entities derive from the 'EntityBase' class, which is not defined as a physical table. The Images table uses both a Foreign Key column of EntityBaseId and discriminator columns which also contain the Id of the parent entity, as shown in the Images table below.

The three classes in question are as follows:

public abstract class EntityBase
{
    public int Id { get; set; } = default;

    public Guid EntityCode { get; set; } = Guid.NewGuid();
    public bool IsActive { get; set; } = false;
    public DateTimeOffset CreateDate { get; set; } = DateTimeOffset.Now;
    public DateTimeOffset UpdateDate { get; set; } = DateTimeOffset.Now;

    public List<Image> Images { get; set; } = new List<Image>();
}

public class Person: EntityBase
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Title { get; set; }
    public string? EmailAddress { get; set; }
    public string? MobileNumber { get; set; }
    public int CurrentAddressId { get; set; }
}

public class Image
{
    public int Id { get; set; }

    public EntityBaseType ParentType { get; set; }
    public string? CloudUri { get; set; }
    public string? CloudThumbnailUri { get; set; }
    public bool IsThumbnail { get; set; }
    public DateTimeOffset CreateDate { get; set; }
    public string? Description { get; set; }
    public int Order { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
    public string? Breakpoints { get; set; }

    public int EntityBaseId { get; set; }
    [JsonIgnore]
    public EntityBase? EntityBase { get; set; }
}

From my dbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Ignore<EntityBase>();
    modelBuilder.Entity<Image>().ToTable("Images");
    modelBuilder.Entity<Job>().ToTable("Jobs");
    modelBuilder.Entity<JobChangeItem>().ToTable("JobChangeItems");
    modelBuilder.Entity<JobNote>().ToTable("JobNotes");
    modelBuilder.Entity<Location>().ToTable("Locations");
    modelBuilder.Entity<Person>().ToTable("People");
    base.OnModelCreating(modelBuilder);
}

public DbSet<Image> Images => Set<Image>();
public DbSet<Job> Jobs => Set<Job>();
public DbSet<JobChangeItem> JobChangeItems => Set<JobChangeItem>();
public DbSet<JobNote> JobNotes => Set<JobNote>();
public DbSet<Location> Locations => Set<Location>();
public DbSet<Person> People => Set<Person>();

And my controller action:

    [HttpPut("{id}")]
    public async Task<IActionResult> PutPerson(int id, Person Person)
    {
        if (id != Person.Id)
        {
            return BadRequest();
        }

        var existingPerson = _context.People
            .Include(i => i.Images)
            .FirstOrDefault(f => f.Id == id);

        existingPerson!.CurrentAddressId = Person!.CurrentAddressId;
        existingPerson!.EmailAddress = Person!.EmailAddress;
        existingPerson!.IsActive = Person!.IsActive;
        existingPerson!.CreateDate = Person!.CreateDate;
        existingPerson!.EntityCode = Person!.EntityCode;
        existingPerson!.FirstName = Person!.FirstName;
        existingPerson!.Id = Person!.Id;
        existingPerson!.LastName = Person!.LastName;
        existingPerson!.MobileNumber = Person!.MobileNumber;
        existingPerson!.Title = Person!.Title;
        existingPerson!.UpdateDate = Person!.UpdateDate;

        foreach (Image image in Person!.Images)
        {
            //image!.EntityBaseId = Person!.Id;
            //_context.Images.Attach(image);
            existingPerson!.Images.Add(image);

        }

        //_context.People.Update(Person);

        //existingPerson.Images = Person!.Images;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            if (!PersonExists(id))
            {
                return NotFound();
            }
            else
            {
                throw ex;
            }
        }

        return NoContent();
    }

SQL People table:

TABLE_NAME  COLUMN_NAME DATA_TYPE
People  Id  int
People  FirstName   nvarchar
People  LastName    nvarchar
People  Title   nvarchar
People  EmailAddress    nvarchar
People  MobileNumber    nvarchar
People  CurrentAddressId    int
People  EntityCode  uniqueidentifier
People  IsActive    bit
People  CreateDate  datetimeoffset
People  UpdateDate  datetimeoffset

SQL Images table:

TABLE_NAME  COLUMN_NAME DATA_TYPE
Images  Id  int
Images  ParentType  int
Images  CloudUri    nvarchar
Images  IsThumbnail bit
Images  CreateDate  datetimeoffset
Images  Description nvarchar
Images  Order   int
Images  Bytes   bigint
Images  Format  nvarchar
Images  Path    nvarchar
Images  PublicId    nvarchar
Images  ResourceType    nvarchar
Images  Signature   nvarchar
Images  Type    nvarchar
Images  Version int
Images  VersionId   nvarchar
Images  Height  int
Images  Width   int
Images  Breakpoints nvarchar
Images  EntityBaseId    int
Images  LocationId  int
Images  PersonId    int
Images  CloudThumbnailUri   nvarchar

I've verified the incoming Person entity contains only one Image. I've also verified the attached existingPerson entity contains no Images before adding the Image and only one Image after the foreach() loop has completed.

Thus far, I've tried many variations of Attach, changing State, Add, Update, etc. to no avail while updating the Person entity and one Image navigation property. Each attempt either produces no Image or duplicate Images in the database.

  • Duplication is of the entire row with the exception of the Image primary key, of course. Both the EntityBaseId and the PersonId are correctly inserted for each row.
  • Adding the image separately via _context.Images.Add(image) produces a record in the database with the EntityBaseId populated, but the discriminator column of PersonId is null, so EF is unable to associate the navigation property with its parent entity.

I've read many, many other stackoverflow posts, other guides and tutorials, but I can't seem to find anything that quite matches the issue I'm having. This is the reason why I'm thinking my issue revolves around inheritance. This speaks to my lack of knowledge regarding TPC (not TPH) inheritance: Since there are technically two foreign keys of EntityBaseId and PersonId in the Images database table, is EF trying to perform an insert twice, once for each key?

It's preferable if I can both understand as well as fix this issue rather than ripping out the inheritance and moving from a monolithic to a more discrete approach. My original thinking was, "Well, if I want all entities to contain a List<Image>, why not have all non-Image entities inherit from an abstract base class?"

Please let me know what required information I am missing from this post; any help is greatly appreciated.

Regards Zac

0 Answers
Related