2

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

Zac
  • 51
  • 1
  • 4
  • What does the database model look like? I don't understand the role of `EntityBaseId`. Also, as for EF, there is no inheritance involved here because EF doesn't know the base class. – Gert Arnold Dec 25 '21 at 09:31
  • Thank you @GertArnold. I meant to write that the pattern was TPC, not TPH, and I added the relevant tables to my question. Also, I used 'https://entityframework.net/tpc' as a basis for the design. I can CRUD parent entities of Person and Location, and the output (including Images) works fine, even though I have duplicates of every image. But I don't know how EF CRUDs the Images navigation property behind the scenes. Heck, I've never even seen an EF code-first that outputs this particular design pattern. – Zac Dec 25 '21 at 14:47
  • Either way, there's no inheritance involved as long as EF doesn't know the base class. All the better, because inheritance (in EF) be better avoided if possible. So you simply have an Images table with FKs to various other tables. No need for an `EntityBaseId`. The FK with value tells which entity is the owner. – Gert Arnold Dec 25 '21 at 14:53
  • Had a feeling you were going to say that. So I'm about one or two attempts removed from changing my design. So, in order to maintain a shared images class for all entities with images, what would be the suggested approach? – Zac Dec 27 '21 at 19:01
  • This is a broad subject, but I always lean to the multiple column approach as in [this question](https://stackoverflow.com/q/7000283/861716). Which is what you have now. Contrary to my comment there, as I see now. Progressive insight I guess. – Gert Arnold Dec 27 '21 at 19:36

0 Answers0