JonPSmith / EfCoreinAction-SecondEdition

Supporting repo to go with book "Entity Framework Core in Action", second edition

Home Page:https://bit.ly/EfCoreBook2

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

On fail-safe navigational collections and adding new elements

JansthcirlU opened this issue · comments

commented

On fail-safe navigational collections and adding new elements

Context

In section 6.1.5. you show an interesting bug where a Book instance is fetched from the database without including its collection of Review instances and where that collection is initialised outside of EF Core's control, causing a merge conflict when saving the changes.

Your proposed solution is to make the setter private and to keep the collection uninitialised everywhere, so that the code will throw an exception when the property is not included with EF. This will catch any unintentional merges.

Caveat when adding new elements

But with this design in mind, how would you handle a business model that has a method to specifically populate its collection? For example, if I have the following ShoppingCart class which exposes an AddToCart(GroceryItem item) method...

public class ShoppingCart
{
  private List<GroceryItem> _items = null!;

  public Guid Id { get; private set; }
  public IEnumerable<GroceryItem> Items
  {
    get => _items;
    private set => _items = value.ToList();
  }

  public void AddToCart(GroceryItem item)
    => _items.Add(item);
}

Adding items to a fetched ShoppingCart

If I query the database and fetch an existing ShoppingCart, including its Items, then EF Core will initialise and populate the _items backing field and ensure that Items is not null. This means that any grocery items associated with that shopping cart will be loaded and that adding new grocery items before saving will work as intended.

If I query the database without including the Items property, then the AddToCart method will throw an exception, which is also acceptable. However...

Adding items to a newly created ShoppingCart

If I at some point create a new ShoppingCart and try to add items to it using the AddToCart method, then it will also throw an exception, since the _items backing field was never populated. I could change the AddToCart method in two ways; either I change the addition line to _items?.Add(item) and have it fail quietly, or I can initialise the _items backing field manually to make sure that the user can add items to a newly created cart, which seems more desirable. However...

Adding items to a fetched ShoppingCart (reprise)

If I now query the database and fetch a ShoppingCart without including the Items property, then the AddToCart method will still initialise the collection if I try to add an item to it, effectively enabling the bug you mentioned in the book.

Final thoughts

As long as a newly created ShoppingCart is saved to the database upon construction and loaded with its (empty but not null) items collection before manipulating it, then none of this would matter. But if that cart needs to be emptied or discarded, I would have to keep track of it and remove it using EF Core. This seems rather unwieldy.

Can these requirements coexist? Is there a better approach to accomplish this interaction?

Hi @JansthcirlU,

I suggest two approaches about collections in EF Core, and each approach is deals with different situations :

  1. I leave a collection relationship as null to make user that if I forget to add a Include I will get an null exception when accessing that collection relationship - see listing 6.3. If I wanted to add a new item to the collection I would either:
    • Read in the entity with a Include on the collection relationship, which set up the collection, and then add a new item to the the collection.
    • If I was creating a new entity I would create a new collection and assign it to the new entity's collection relationship
  2. When talking about Domain-Driven Design (DDD) I suggest making the collection relationship read-only to follow the DDD approach - see listing 13.1. If I wanted to add a new item to the collection I would either:
    • Read in the entity with a Include on the collection relationship, and call the access method to add the new item.
    • If I was creating a new DDD entity I would have to provide the collection through the entity's constructor.

Your "Caveat when adding new elements" section shows a collection relationship of type IEnumerable<GroceryItem> but you allow the settings of a collection relationship. This seems to have a mixture of both approaches, but doesn't meet the DDD rule of data in the entity should only done by access methods or constructor.

I hope that make things clearer.

commented

Thank you for your reply! Yes, this did clear things up. I did not consider the fact that the constructor with parameters would only be called if a new instance was created manually rather than through an EF Core query, in which case there would be no side-effects to initialising the collections.