Page 14 - MSDN Magazine, April 2018
P. 14

base class and the tweak I made to the PostalAddress to force EF Core to take care of the problem without putting the onus on SalesOrder. The magic happens in the override of the SaveChanges method of my DbContext class, shown in Figure 5.
From the collection of entries that the DbContext is tracking, SaveChanges will iterate through those that are SalesOrders flagged to be added to the database, and will make sure they get populated as their empty counterparts.
What About Querying Those Empty Owned Types?
Having satisfied the need for EF Core to store null value objects, it’s now time to query them back from the database. But EF Core resolves those properties in their empty state. Any ShippingAddress or BillingAddress that was originally null comes back as an instance with null values in its properties. After any query, I need my logic to replace any empty PostalAddress properties with null.
I spent quite some time looking for an elegant way to achieve this. Unfortunately, there isn’t yet a lifecycle hook to modify objects as they’re being materialized from query results. There’s a replaceable service in the query pipeline called CreateReadValueExpression in the internal EntityMaterializerSource class, but that can only be used on scalar values, not objects. I tried numerous other approaches that were more and more complicated, and finally had a long talk with myself about the fact that this is a temporary workaround, so I can accept a simpler solution even if it is has a little bit of code smell. And this task isn’t too difficult to control if your queries are encap- sulated in a class dedicated to making EF Core calls to the database.
I named the method FixOptionalValueObjects:
private static void FixOptionalValueObjects (SalesOrder order) {
if (order.ShippingAddress.IsEmpty ()) { order.SetShippingAddress (null); } if (order.BillingAddress.IsEmpty ()) { order.SetBillingAddress (null); }
}
Now I have a solution in which the user can leave value objects null and let EF Core store and retrieve them as non-nulls, yet return them to my code base as nulls anyway.
Replacing Value Objects
I mentioned another limitation in the current version of EF Core 2, which is the inability to replace owned entities. Value objects are by definition immutable. So, if you need to change one, the only way is to replace it. Logically this means that you’re modifying the SalesOrder,
Figure 6 Making SaveChanges Comprehend Replaced Owned Types by Marking Them as Modified
just as if you had changed its OrderDate property. But because of the way that EF Core tracks the owned entities, it will always think the replacement is added, even if its host, SalesOrder, for example, is not new.
I made a change to the SaveChanges override to fix this problem (see Figure 6). The override now filters for SalesOrders that are added or modified, and with the two new lines of code that modify the state of the reference properties, ensures that ShippingAddress and BillingAddress have the same state as the order—which will either be Added or Modified. Now modified SalesOrder objects will also now be able to include the values of the ShippingAddress and BillingAddress properties in their UPDATE commands.
Value objects are by definition immutable. So, if you need to change one, the only way is to replace it.
This pattern works because I’m saving with a different instance of OrderContext than I queried, which therefore doesn’t have any preconceived notion of the state of the PostalAddress objects. You can find an alternate pattern for tracked objects in the comments of the GitHub issue at bit.ly/2sxMECT.
Pragmatic Solution for the Short-Term
If changes to allow optional owned entities and replacing owned entitieswerenotonthehorizon,I’dmostlikelytakestepstocreatea separate data model to handle the data persistence in my software. But this temporary solution saves me that extra effort and invest- ment and I know that soon I can remove my work-arounds and easily map my domain models directly to my database letting EF Core define the data model. I was happy to invest the time, effort and thought into coming up with the work-arounds so I can use value objects and EF Core 2 when designing my solutions—and help others be able to do the same.
Note that the download that accompanies this article is housed in a console app to test out the solution as well as persist the data to a SQLite database. I’m using the database rather than just writ- ing tests with the InMemory provider because I wanted to inspect the database to be 100 percent sure that the data was getting stored the way I expected. n
Julie lerman is a Microsoft Regional Director, Microsoft MVP, software team coach and consultant who lives in the hills of Vermont. You can find her presenting on data access and other topics at user groups and conferences around the world. She blogs at the thedatafarm.com/blog and is the author of “Programming Entity Framework,” as well as a Code First and a DbContext edition, all from O’Reilly Media. Follow her on Twitter: @julielerman and see her Pluralsight courses at juliel.me/PS-Videos.
Thanks to the following Microsoft technical expert for reviewing this article: Andriy Svyryd
public override int SaveChanges () {
foreach (var entry in ChangeTracker.Entries ().Where (
e => e.Entity is SalesOrder &&
(e.State == EntityState.Added || e.State == EntityState.Modified))) { if (entry.Entity is SalesOrder order) {
if (entry.Reference ("ShippingAddress").CurrentValue == null) { entry.Reference ("ShippingAddress").CurrentValue = PostalAddress.Empty ();
}
if (entry.Reference ("BillingAddress").CurrentValue == null) {
entry.Reference ("BillingAddress").CurrentValue = PostalAddress.Empty (); }
entry.Reference ("ShippingAddress").TargetEntry.State = entry.State;
entry.Reference ("BillingAddress").TargetEntry.State = entry.State; }
}
return base.SaveChanges (); }
10 msdn magazine
Data Points


































































































   12   13   14   15   16