TanvirArjel
TanvirArjel

Reputation: 32159

Updating many to many in Entity Framework core

Here are the relevant classes and their configuration:

public class Product 
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long ProductId { get; set; }

    [Required]
    public string ProductName { get; set; }

    public ICollection<ProductSpecification> ProductSpecifications { get; set; }
} 

public class ProductAttributeValue 
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long ProductAttributeValueId { get; set; }

    [Required]
    public string ProductAttributeValueName { get; set; }

    public ICollection<ProductSpecification> ProductSpecifications { get; set; }
}

public class ProductSpecification
{
    public long ProductId { get; set; }
    public long ProductAttributeValueId { get; set; }

    public string Note { get; set; }

    public Product Product { get; set; }
    public ProductAttributeValue ProductAttributeValue { get; set; }
}

// Configuration in the dbConext
modelBuilder.Entity<ProductSpecification>().HasKey(ps => new { ps.ProductId, ps.ProductAttributeValueId });
modelBuilder.Entity<ProductSpecification>().HasOne(p => p.Product).WithMany(ps => ps.ProductSpecifications).HasForeignKey(ps => ps.ProductId);
modelBuilder.Entity<ProductSpecification>().HasOne(pav => pav.ProductAttributeValue).WithMany(ps => ps.ProductSpecifications).HasForeignKey(ps => ps.ProductAttributeValueId);

In the controller :

public async Task<IActionResult> UpdateProduct([FromRoute] long id, [FromBody] Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (id != product.ProductId)
    {
        return BadRequest();
    }

    Product productToBeUpdated = await _unitOfWork.Repository<Product>().GetEntityList(p => p.ProductId == id).Include(p => p.ProductSpecifications).SingleOrDefaultAsync();

    if (productToBeUpdated == null)
    {
        return NotFound();
    }

    foreach (ProductSpecification productSpecification in productToBeUpdated.ProductSpecifications.ToList())
    {
        productToBeUpdated.ProductSpecifications.Remove(productSpecification);
    }

    productToBeUpdated.ProductSpecifications = product.ProductSpecifications;

    productToBeUpdated.ModifiedOn = DateTime.UtcNow;
    await _unitOfWork.SaveChangesAsync();

    return Ok(true);
 }

I have also tried:

foreach (ProductSpecification productSpecification in productToBeUpdated.ProductSpecifications.ToList())
{
    _unitOfWork.Repository<ProductSpecifications>().DeleteEntity(productSpecification);
}

Both of them throwing the following exception:

The instance of entity type 'ProductSpecification' cannot be tracked because another instance with the same key value for {'ProductId', 'ProductAttributeValueId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.

Could not find where the problem actually lies! Any help will be highly appreciated!

Upvotes: 2

Views: 2491

Answers (1)

TanvirArjel
TanvirArjel

Reputation: 32159

Well problem is finally identified! In Entity Framework 6.x, during many to to many navigation properties update, we can clear/delete the existing child list and then add the new child list to the parent and finally update the parent and parent is updated with the new child list.

But in Entity Framework Core, we cannot do the same.The error message suggest that we cannot have same child with the same key value more than once. We have to maintain the uniqueness of entities in the Entity Framework transaction.

That's why we have to first identified which existing children are being deleted and which children are being newly added during update operation. This will make Entity framework Transaction state unique.

So the Update method will be as follows:

public async Task<IActionResult> UpdateProduct([FromRoute] long id, [FromBody] Product product)
{
    if (!ModelState.IsValid)
    {
       return BadRequest(ModelState);
    }

    if (id != product.ProductId)
    {
       return BadRequest();
    }

    Product productToBeUpdated = await _unitOfWork.Repository<Product>().GetEntityList(p => p.ProductId == id).Include(p => p.ProductSpecifications).SingleOrDefaultAsync();
    if (productToBeUpdated == null)
    {
        return NotFound();
    }

    _mapper.Map(product, productToBeUpdated); // If you use AutoMapper than ignore the many-to-many navigation property in the mapping

   List<ProductSpecification> productSpecificationsToBeDeleted = productToBeUpdated.ProductSpecifications.Where(c1 => product.ProductSpecifications.All(c2 => c2.ProductAttributeValueId != c1.ProductAttributeValueId)).ToList();
   foreach (ProductSpecification productSpecification in productSpecificationsToBeDeleted)
   {
                productToBeUpdated.ProductSpecifications.Remove(productSpecification);
   }

   List<ProductSpecification> productSpecificationsToBeAdded = product.ProductSpecifications.Where(c1 => productToBeUpdated.ProductSpecifications.All(c2 => c2.ProductAttributeValueId != c1.ProductAttributeValueId)).ToList();
   foreach (ProductSpecification productSpecification in productSpecificationsToBeAdded)
   {
                productToBeUpdated.ProductSpecifications.Add(productSpecification);
   }

   productToBeUpdated.ModifiedOn = DateTime.UtcNow;
   await _unitOfWork.SaveChangesAsync();
   return Ok(true);
}

Upvotes: 2

Related Questions