0

I have a Category class:

public class Category
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }
}

I also have a Subcategory class:

public class Subcategory
{
    public int SubcategoryId { get; set; }
    public Category Category { get; set; }
    public string SubcategoryName { get; set; }
}

And a Flavor class:

public class Flavor
{
    public int FlavorId { get; set; }
    public Subcategory Subcategory { get; set; }
    public string FlavorName { get; set; }
}

Then I also have Filling and Frosting classes just like the Flavor class that also have Category and Subcategory navigation properties.

I have a Product class that has a Flavor navigation property.

An OrderItem class represents each row in an order:

public class OrderItem
{
    public int OrderItemId { get; set; }
    public string OrderNo { get; set; }
    public Product Product { get; set; }
    public Frosting Frosting { get; set; }
    public Filling Filling { get; set; }
    public int Quantity { get; set; }
}

I'm having issues when trying to save an OrderItem object. I keep getting DbUpdateException: An error occurred while saving entities that do not expose foreign key properties for their relationships. with the Inner Exception being OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. I've stepped through my code several times and I can't find anything that modifies or deletes any entities loaded from the database. I've been able to save the OrderItem, but it creates duplicate entries of Product, Flavor, Subcategory and Category items in the DB. I changed the EntityState of the OrderItem to Modified, but that throws the above exception. I thought it might have been the fact that I have Product, Frosting and Filling objects all referencing the same Subcategory and Category objects, so I tried Detaching Frosting and Filling, saving, attaching, changing OrderItem entity state to Modified and saving again, but that also throws the above exception.

The following statement creates duplicates in the database:

db.OrderItems.Add(orderItem);

Adding any of the following statements after the above line all cause db.SaveChanges(); to throw the mentioned exception (both Modified and Detached states):

db.Entry(item).State = EntityState.Modified;
db.Entry(item.Product.Flavor.Subcategory.Category).State = EntityState.Modified;
db.Entry(item.Product.Flavor.Subcategory).State = EntityState.Modified;
db.Entry(item.Product.Flavor).State = EntityState.Modified;
db.Entry(item.Product).State = EntityState.Modified;

Can someone please give me some insight? Are my classes badly designed?

2
  • You really should show the code that throws the exception. Commented Nov 6, 2018 at 19:36
  • Added it to the bottom of the question. Thanks. Commented Nov 6, 2018 at 19:52

1 Answer 1

1

The first thing to check would be how the entity relationships are mapped. Generally the navigation properties should be marked as virtual to ensure EF can proxy them. One other optimization is that if the entities reference SubCategory then since SubCats reference a Category, those entities do not need both. You would only need both if sub categories are optional. Having both won't necessarily cause issues, but it can lead to scenarios where a Frosting's Category does not match the category of the Frosting's SubCategory. (Seen more than enough bugs like this depending on whether the code went frosting.CategoryId vs. frosting.SubCategory.CategoryId) Your Flavor definition seemed to only use SubCategory which is good, just something to be cautious of.

The error detail seems to point at EF knowing about the entities but not being told about their relationships. You'll want to ensure that you have mapping details to tell EF about how Frosting and SubCategory are related. EF can deduce some of these automatically but my preference is always to be explicit. (I hate surprises!)

public class FrostingConfiguration : EntityTypeConfiguration<Frosting>
{
  public FlavorConfiguration()
  {
    ToTable("Flavors");
    HasKey(x => x.FlavorId)
      .Property(x => x.FlavorId)
      .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

    HasRequired(x => x.SubCategory)
      .WithMany()
      .Map(x => x.MapKey("SubCategoryId");
  }
}

Given your Flavor entity didn't appear to have a property for the SubCategoryId, it helps to tell EF about it. EF may be able to deduce this, but with IDs and the automatic naming conventions it looks for, I don't bother trying to remember what works automagically.

Now if this is EF Core, you can replace the .Map() statement with:

.ForeignKey("SubCategoryId");

which will set up a shadow property for the FK.

If SubCats are optional, then replace HasRequired with HasOptional. The WithMany() just denotes that while a Flavor references a sub category, SubCategory does not maintain a list of flavours.

The next point of caution is passing entities outside of the scope of the DBContext that they were loaded. While EF does support detaching entities from one context and reattaching them to another, I would argue that this practice is almost always far more trouble than it is worth. Mapping entities to POCO ViewModels/DTOs, then loading them on demand again when performing updates is simpler, and less error-prone then attempting to reattach them. Data state may have changed between the time they were initially loaded and when you go to re-attach them, so fail-safe code needs to handle that scenario anyways. It also saves the hassle of messing around with modified state in the entity sets. While it may seem efficient to not load the entities a second time, by adopting view models you can optimize reads far more efficiently by only pulling back and transporting the meaningful data rather than entire entity graphs. (Systems generally read far more than they update) Even for update-heavy operations you can utilize bounded contexts to represent large tables as smaller, simple entities to load and update a few key fields more efficiently.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for this! Everything I've done so far with Entity has worked automagically, as you put it. I need to learn how to be more explicit for occasions like this. As my app is for one user, I had been passing around Entity objects with TempData, not worrying about possible data state changes, so I guess Entity was just being safe. You're right, messing with Entity states is a hassle. Best just to reload data.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.