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