Page 12 - MSDN Magazine, April 2018
P. 12

alternate code will split them out to separate tables and avoid the nullability problem completely:
modelBuilder.Entity<SalesOrder> ().OwnsOne (
s => s.BillingAddress).ToTable("BillingAddresses");
modelBuilder.Entity<SalesOrder> ().OwnsOne (
s => s.ShippingAddress).ToTable("ShippingAddresses");
Creating a SalesOrder in Code
Inserting a sales order with both the billing address and shipping address is simple:
private static void InsertNewOrder() {
var order=new SalesOrder{OrderDate=DateTime.Today, OrderTotal=100.00M}; order.SetShippingAddress (PostalAddress.Create (
"One Main", "Burlington", "VT", "05000")); order.SetBillingAddress (PostalAddress.Create ( "Two Main", "Burlington", "VT", "05000"));
using(var context=new OrderContext()){ context.SalesOrders.Add(order); context.SaveChanges();
} }
But let’s say that my business rules allow an order to be stored even if the shipping and billing address haven’t yet been entered, and a user can complete the order at another time. I’ll comment out the code that fills the BillingAddress property:
// order.BillingAddress=new Address("Two Main","Burlington", "VT", "05000");
When SaveChanges is called, EF Core tries to figure out what the properties of the BillingAddress are so that it can push them into the SalesOrder table. But it fails in this case because BillingAddress is null. Internally, EF Core has a rule that a conventionally mapped owned type property can’t be null.
EF Core is assuming that the owned type is available so that its properties can be read. Developers may see this as a showstopper for being able to use value objects or, worse, for being able to use EF Core, because of how critical value objects are to their soft- ware design. That was how I felt at first, but I was able to create a work-around.
Temporary Work-Around
to Allow Null Value Objects
The goal of the work-around is to ensure that EF Core will receive a ShippingAddress, BillingAddress or other owned type, whether or not the user supplied one. That means the user isn’t forced to supply a shipping or billing address just to satisfy the per- sistence layer. If the user doesn’t supply one, then a PostalAddress
Figure 4 The IsEmpty Method Added to the ValueObject Base Class
object with null values in its properties will be added in by the DbContext when it’s time to save a SalesOrder.
I made a minor adaptation to the PostalAddress class by adding a second factory method, Empty, to let the DbContext easily cre- ate an empty PostalAddress:
public static PostalAddress Empty() {
return new PostalAddress(null,null,null,null);
}
Additionally, I enhanced the ValueObject base class with a new method, IsEmpty, shown in Figure 4, to allow code to easily deter- mine if an object has all null values in its properties. IsEmpty leverages code that already exists in the ValueObject class. It iterates through the properties and if any one of them has a value, it returns false, indicating that the object is not empty; otherwise, it returns true.
The goal of the work-around is to ensure that EF Core will receive a PostalAddress whether or not the user supplied one.
But my solution for allowing null owned entities wasn’t yet complete. I still needed to use all of this new logic to ensure that new SalesOrders would always have a ShippingAddress and a BillingAddress in order for EF Core to be able to store them into the database. When I initially added this last piece of my solution, I wasn’t happy with it because that last bit of code (which I won’t bother sharing) was making the SalesOrder class enforce EF Core’s rule—the bane of domain-driven design.
Voila! An Elegant Solution
Luckily, I was speaking at DevIntersection, as I do every fall, where Diego Vega and Andrew Peters from the EF team were also pre- senting. I showed them my work-around and explained what was bothering me—the need to enforce non-null ShippingAddress and BillingAddress in the SalesOrder—and they agreed. Andrew quickly came up with a way to use the work I had done in the ValueObject
Figure 5 Overriding SaveChanges to Provide Values to Null Owned Types
public override int SaveChanges() {
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.Entity is SalesOrder && e.State == EntityState.Added))
{
if (entry.Entity is SalesOrder) {
if (entry.Reference("ShippingAddress").CurrentValue == null) {
entry.Reference("ShippingAddress").CurrentValue = PostalAddress.Empty(); }
if (entry.Reference("BillingAddress").CurrentValue == null) {
entry.Reference("BillingAddress").CurrentValue = PostalAddress.Empty(); }
}
return base.SaveChanges(); }
public bool IsEmpty () {
Type t = GetType (); FieldInfo[] fields = t.GetFields
(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); foreach (FieldInfo field in fields)
{
object value = field.GetValue (this); if (value != null)
{
return false; }
}
return true; }
8 msdn magazine
Data Points


































































































   10   11   12   13   14