Home > Back-end >  Adding Owned Entities With Many Relation Cause Concurrency Exception Due to Update instead of Insert
Adding Owned Entities With Many Relation Cause Concurrency Exception Due to Update instead of Insert

Time:10-28

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....

  • Related