BioData41
BioData41

Reputation: 982

EF Core 2.0 Trouble 'Cascading' Inserts for Related Entities When Updating Principle Entity

ASP.NET Core 2 Web application using a REST API. Currently using sqlite3 for development database. (Also tried migrating to SQL Server and got same results as below).

I'm sending an entity to web client, the client makes changes to the entity that involve adding a new related entity and then that updated principle entity gets sent back as json in body of PUT a request.

I was hoping the new related entity would get created automatically, but this is not happening. The simple properties on the principle entity are updated properly, but not reference properties. I'm not getting any exceptions or anything - it just seems to be ignoring the reference properties.

Simplified Classes (I removed other properties that shouldn't affect the relationship):

public partial class DashboardItem {
    public int Id { get; set; }
    public int? DataObjectId { get; set; }
    public DataObject DataObject { get; set; }

}

public partial class DataObject {
    public int Id { get; set; }
}

Portion of DbContext Fluent API for associated property:

        modelBuilder.Entity<DashboardItem>(entity => {
            entity.HasOne(p => p.DataObject)
            .WithMany()
            .HasForeignKey(p => p.DataObjectId);
        });

Controller Method for PUT:

    [HttpPut("{id}")]
    public async Task<IActionResult> PutDashboardItem([FromRoute] int id, [FromBody] DashboardItem entity)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != entity.Id)
        {
            return BadRequest();
        }

        _context.Entry(entity).State = EntityState.Modified;

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

The simplified json (without all the other properties) would look like this (I've tried different variations of have the foreign key "DataObjectId" removed from the json, set to null, or set to zero in case that might be interferring.):

{
  Id:1,
  DataObjectId:null,
  DataObject:{
    Id: 0
  }
}

When debugging in the controller action method, the existing "DashboardItem" principle entity created from the request body has the reference property "DataObject" populated before getting added to the DbContext, but the new DataObject never gets created in the database. There is only a SQL UPDATE statement issued for DashboardItem and no INSERT for DataObject.

I've also tried making the controller method synchronous instead of async, using DbContext.SaveChanges() instead of .SaveChangesAsync(), since there used to be a problem with that in earlier versions of EF Core related to creating related entities, even though I'm using 2.0 which already has a fix for that. Same result.

This EFCore Doc sounds like it should just work out of the box.

This has worked for me in a prior project. What am I missing here?

Upvotes: 1

Views: 1988

Answers (1)

BioData41
BioData41

Reputation: 982

Basically, my mistake was in assuming the process of updating data was much simpler than it actually is when sending the updated data from a client in a web application.

After digging a lot more, it seems that the following line in my controller method for handling the PUT request is the problem:

_context.Entry(entity).State = EntityState.Modified;

Setting the entity entry state to Modified in this way results in Entity Framework Core ignoring the reference properties for the related objects - the SQL UPDATE generated will only address the columns in the entity table.

This simple summary eventually got me started down the right path.


Summarizing what I've now learned:

This controller method is dealing with a 'detached' entity that was edited and sent back from the client. The DbContext is not yet tracking this entity since I get a new instance of the context with each http request (hence the entity is considered 'detached'). Because it is not being tracked yet, when it is added to the DbContext, the context needs to be told whether this entity has been changed and how to treat it.

There are several ways to tell the DbContext how to handle the detached entity. Among those:

(1) setting the entity state to EntityState.Modified will result in ALL properties being included in the SQL update (whether they've actually changed or not), EXCEPT for the reference properties for related entities:

      _context.Entry(entity).State = EntityState.Modified;

(2) adding the entity with a call to DbContext.Update will do the same as above, but will include the reference properties, also include ALL properties on those entities in the update, whether they've changed or not:

       _context.Update(entity) 

Approach #2 got things working for me, where I was just trying to get the new related child entity to be created in the Update to its parent.

Beyond that, DbContext.Attach() and DbContext.TrackGraph sound like thy provide more find-grained control over specifying what specific properties or related entities to include in the update.

Upvotes: 1

Related Questions