Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

nested Owned Type not saved when Updating in the database

Tags:

c#

ef-core-2.2

I have following classes:

Order:

public class Order {
  private Order()
  {
      //some code to initialise object
  }
  //more properties
  public Prepayment Prepayment { get; private set; }
  //more methods and properties
}

Prepayment:

public class Prepayment:ValueObject<Prepayment>
{
    private Prepayment()
    {   
    }

    public Money AmountPrepaid { get; private set; }
    public bool HasPrepaymentBeenTaken => AmountPrepaid.Amount > 0;
}

Money:

public class Money {
        private Money()
        {
        }
        private Money(decimal amount)
        : this()
        {
            Amount = amount;
        }
        public decimal Amount { get; private set; }
}

Then I the Order class is mapped to the database in following way:

modelBuilder.Entity<Order>()
                .OwnsOne(x => x.Prepayment,
                    prepayment =>
                    {
                        prepayment.OwnsOne(x => x.AmountPrepaid,
                            amountPrepaid =>
                            {
                                amountPrepaid.Property(x => x.Amount)
                                    .HasColumnName("PrepaymentAmount")
                                    .HasColumnType("decimal(7,2)");
                            });
                    });

Repository code to SaveChanges:

public async Task<int> SaveAsync(Order order)
{
    if (order.Id == 0)
    {
        await _context.AddAsync(order);
    }
    else
    {
        _context.Update(order);
    }

    return await _context.SaveChangesAsync();
}

Please understand, I removed all the not important properties from the code, to make the example more clear.

Above code works well for INSERT scenario, where Prepayment -> Money -> Amount is properly saved into the database. UPDATE though doesn't seem to be reflected in the database.

Please note, I have quite a few Owned Types in that model, and all of them are working well. The only difference as far as I can say is the fact that Prepayment property has another nested Owned type - Money.

Order class object passed to repository, is first pulled from the database, then changes are applied on that instance, and finally saved back to the database. Other properties like Customer not mentioned in the example, is also a OwnedType and UPDATE works as expected.

Just in case - the code used to retrieve the object prior to update:

public async Task<Order> GetOrderByIdAsync(int orderId)
{
    var result = (from order in _context.Orders
                  where order.Id == orderId
                  select order).Include(x => x.OrderLines);

    return await result.FirstOrDefaultAsync();
}

The exact version of Entity Framework Core I am using is: 2.2.0

Any help would be highly appreciated. Thank you.

EDIT:

The code that updates the data looks like this:

public async Task<int> Handle(EditOrderCommand request, CancellationToken cancellationToken)
{
    var order = await _orderRepository.GetOrderByIdAsync(request.Id);

    var customer = new Customer(
        request.FirstName,
        request.LastName,
        request.TelephoneNumber);

    var prepayment = new Prepayment(
        Money.SomeMoney(
            request.PrepaymentAmount
            )
        );

    order.ApplyChanges(
            request.UserId, 
            request.AdditionalInformation,
            collectionDate,
            customer,
            prepayment);

    await _orderRepository.SaveAsync(order);

    return order.Id;
}

And part of the ApplyChanges method that sets prepayment:

private void SetPrepayment(Prepayment prepayment)
{
    Prepayment = prepayment ?? throw new ArgumentNullException(nameof(prepayment));
}
like image 474
madoxdev Avatar asked Feb 24 '19 21:02

madoxdev


1 Answers

The issue has something in common with the nested owned entity type.

But the general problem is that the way you are using owned entity types is violating the EF Core rules, thus the behavior is undefined - sometimes it might work, sometimes not, sometimes even throw exceptions etc.

Owned entity types cannot be used to implement value objects, because even they are "owned", by EF Core terminology they are still "entities" (with hidden shadow PK), so they are tracked by reference and follow the same rules as other entity references -most importantly, there must be only one object instance per defining navigation PK.

In short, they are supposed to be allocated once and then mutated via their primitive properties. While what are you doing is mutating the references with immutable objects.

It's hard to give you good advice because of the aforementioned EF Core rule violations. The only working workaround is to make sure all original object references are not tracked by the context.

For instance, if GetOrderByIdAsync implementation uses AsNoTracking query, hence neither order nor order.Prepayment and order.Prepayment.AmountPrepaid instances are tracked by the _context, then _context.Update(order) will work.

It will also work if you manually detach them before calling ApplyChanges (requires access to the db context):

_context.Entry(order).State = EntityState.Detached;
_context.Entry(order.Prepayment).State = EntityState.Detached;
_context.Entry(order.Prepayment.AmountPrepaid).State = EntityState.Detached;

order.ApplyChanges(...);

await _orderRepository.SaveAsync(order); // _context.Update(order);

Looks like AsNoTracking is the better option. You can make it default for all queries by setting ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; inside your db context constructor and use AsTracking() where needed.

like image 184
Ivan Stoev Avatar answered Nov 04 '22 06:11

Ivan Stoev