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.
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));
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With