Stefan
Stefan

Reputation: 672

EF Core avoid duplicate addresses

This seems like a simple task but I still didn't find a solution.

I have a model class that represents a address and a class for orders:

internal class Address
{
    [Key]
    public Guid ID { get; set; }
    /*fields for street, city, etc*/
}

internal class Order
{
    [Key]
    public Guid ID { get; set; }
    /*other fields*/
    public Address ShipToAddress { get; set; }
    public Address BillToAddress { get; set; }
}

The problem is, that in like 95% of the cases, the ship to and the bill to address are the same.

Even if I set both fields to the same object, Entity Framework creates 2 rows in the database containing the exact same data (except for the ID of course).

Is there a (simple) way to avoid creating those duplicates?

EDIT

This is how the order is created / inserted:

var billToAddress = new Address();
var shipToAddress = billToAddress;

var order = new Order
{
    ShipToAddress = shipToAddress,
    BillToAddress = billToAddress,
};

_dbContext.Orders.Add(order);
_dbContext.SaveChanges();

Upvotes: 1

Views: 785

Answers (1)

Chris Schaller
Chris Schaller

Reputation: 16689

This behaviour occurs when the Same Address object is added to the Order object when the order object is Added to the Orders collection.

By default, when Creating a new object, all other navigation objects that do not have their [Key] fields set, are assumed that they too need to be added to the database, this pattern can happen as well with other types of lookup fields and relationships.

  • In cases like yours, this will always result in two records, even though they might have been the same object reference.

There are a few solutions to this:

  1. Accept this behaviour and move on, if you always assume the two records will be separate, in the user interface you can write to both records at the same time, or until the user unticks a box that says "Billing and Shipping address are the same"
  2. Change the workflow so that when adding Address objects to the ShipTo or BillTo address they have already been committed to the database (their ID field will be set) then you can add the same Address to both fields without any issues.

    • You could even change the process so that Order is saved to the database first.
    • You should handle the "cancel situation", if the save on the order fails you may have to manually delete the address that was created
      • You can use Transactions to assist in this scenario.
    • This is a bit more involved, but you could make an Address Factory method that returns a new Address record that has already been committed to the database. Without using the factory pattern, your code might look like this:

      var order = new Order();
      
      var billToAddress = new Address();
      _dbContext.Addresses.Add(billToAddress);
      _dbContext.SaveChanges();
      
      order.ShipToAddress = billToAddress;
      order.BillToAddress = billToAddress;
      
      _dbContext.Orders.Add(order);
      _dbContext.SaveChanges();
      
  3. You can override the SaveChanges method on the DbContext so that before save, you can iterate through the Order changes, detect when Billto and ShipTo address objects are the same, then remove one before executing base.SaveChanges() then add the reference back and then call base.SaveChanges again.

Option 1 is a design choice, making this decision means the database side is always pretty simple, the complexity is in the user interface.

Although I have done option 3 a few times in the past, I prefer option 2 (when option 1 is not acceptable) as it keeps this logic close to the other logic associated with Orders and Addresses, long term option 2 is easier to maintain.

You could look at another design pattern altogether, I like to make Order have a collection of Address records and each Address has an AddressType which is an enum:

Billing, Sender, Receiver...

Then in your UI, an Address with AddressType = AddressTypes.Billing address is always created first, because we always need this, the user can add new address records as they want to, when only Billing address exists, all other address operations will use this reference.

UI Validation and database constraints can be implemented to ensure that duplicate address records with the same type are no created.

Upvotes: 1

Related Questions