On fail-safe navigational collections and adding new elements
JansthcirlU opened this issue · comments
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 :
- 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
- Read in the entity with a
- 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.
- Read in the entity with a
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.
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.