I have an issue where adding an owned entity causes EF Core to issue an UPDATE
statement instead of an INSERT
, causing a Concurrency Exception.
I have the following classes (simplified to keep the question shorter):
public abstract class PaymentDemand
{
public Guid Id { get; set; }
public SettlementTransactions? SettlementTransactions { get; set; }
// And more properties
}
public class SettlementTransactions
{
public List<SettlementAllowance> ConsumedAllowances { get; set; } = new List<SettlementAllowance>();
public List<SettlementCharge> GeneratedCharges { get; set; } = new List<SettlementCharge>();
}
public class SettlementAllowance
{
public Guid PaymentDemandId { get; set; }
/// <summary>The type of the transaction.</summary>
public string TransactionType { get; set; }
// And more properties
}
Then I have setup the payment demand as such:
public void Configure(EntityTypeBuilder<PaymentDemand> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).ValueGeneratedNever();
builder.OwnsOne(p => p.SettlementTransactions, transactionBuilder =>
{
transactionBuilder.OwnsMany(a => a.ConsumedAllowances, allowanceBuilder =>
{
allowanceBuilder.WithOwner().HasForeignKey(t => t.PaymentDemandId);
allowanceBuilder.HasKey(p => p.AccountTransactionId);
allowanceBuilder.Property(p => p.Amount).HasColumnType("Money");
allowanceBuilder.ToTable("SettlementAllowances");
});
transactionBuilder.OwnsMany(c => c.GeneratedCharges, chargesBuilder =>
{
chargesBuilder.WithOwner().HasForeignKey(t => t.PaymentDemandId);
chargesBuilder.HasKey(p => p.AccountTransactionId);
chargesBuilder.Property(p => p.AccountTransactionId).ValueGeneratedNever();
chargesBuilder.Property(p => p.Amount).HasColumnType("Money");
chargesBuilder.ToTable("SettlementCharges");
});
});
builder.Navigation(t => t.SettlementTransactions).IsRequired();
}
Lastly I have the code that does the saving.
public async Task DoThings(DbContext context)
{
var demand = await context.PaymentDemands
.Include(st => st.SettlementTransactions)
.FirstOrDefaultAsync(d => d.Id == id, default);
if (demand == null)
{
throw new InvalidOperationException();
}
demand.SettleDate = DateTime.UtcNow;
demand.SettlementTransactions ??= new SettlementTransactions();
demand.SettlementTransactions.ConsumedAllowances.Add(new SettlementAllowance{ TransactionType = "Blah" });
await context.SaveChangesAsync(); // THROWS.
}
The change tracker seems to be working out what it should do (from the log):
DetectChanges starting for 'SqlBillingContext'.
2 entities were added and 0 entities were removed from navigation 'SettlementTransactions.ConsumedAllowances' on entity with key '{PaymentDemandId: d00544fe-c6c5-4b1d-a438-81508332af2d}'.
Context 'SqlBillingContext' started tracking 'SettlementAllowance' entity with key '{AccountTransactionId: 296596b3-d45d-4753-b2f5-0ba7566f6800}'.
Context 'SqlBillingContext' started tracking 'SettlementAllowance' entity with key '{AccountTransactionId: 7395965f-7fda-46d9-9e3b-3b428d4a7dce}'.
But for some reason it ends up with an Update just after:
Executing update commands individually as the number of batchable commands (2) is smaller than the minimum batch size (4).
Executing DbCommand [Parameters=[@p11='296596b3-d45d-4753-b2f5-0ba7566f6800', @p0='2022-10-25T09:34:41.7143804 00:00', @p1='49.5' (Precision = 3) (Scale = 1), @p2=NULL (Size = 4000), @p3='2022-12-24T09:18:36.3176378 00:00' (Nullable = true), @p4='6ca8c244-4438-4476-b417-ed501c378c0e' (Nullable = true), @p5='12032' (Nullable = true), @p6='d00544fe-c6c5-4b1d-a438-81508332af2d', @p7='df99b62b-d1b6-4d58-8c69-4df7eef33b61' (Nullable = true), @p8='d00544fe-c6c5-4b1d-a438-81508332af2d' (Nullable = true), @p9='2022-11-24T09:18:37.3176378 00:00' (Nullable = true), @p10='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [SettlementAllowances] SET [AccountingTime] = @p0, [Amount] = @p1, [Description] = @p2, [EndTime] = @p3, [InvoiceId] = @p4, [InvoiceNumber] = @p5, [PaymentDemandId] = @p6, [PaymentId] = @p7, [SourcePaymentDemandId] = @p8, [StartTime] = @p9, [TransactionType] = @p10
WHERE [AccountTransactionId] = @p11;
SELECT @@ROWCOUNT;
I really don´t understand whats going on here. Any suggestions, ideas or obvious reasons why this is NOT working as I hope it would?
CodePudding user response:
As is often the case, writing these post is almost like rubber ducking.
The issue in my case is that I have the primary key of the entity I create (since im basically moving it). Thus EF thinks that it needs to generate a key of one does not exist, and one is provided, it assumes the record exists (default behaviour).
The solution was to simply add a Generation strategy of NEVER:
allowancesBuilder.Property(p => p.AccountTransactionId).ValueGeneratedNever();
Which I already had on the charges and the payments!.
The default is apparently ValueGeneratedSometimes
....