ExecuteUpdate over projection uses derived-table copy of updated column, causing possible lost updates

Aperta
#38,505 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 2 giorni

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
52/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Tranquilla
Stack tecnologico
csharp, postgresql
Ambito
databases

Direzione di ricerca

Inizia riproducendo la query LINQ ExecuteUpdateAsync fornita e ispezionando l’SQL PostgreSQL generato. Segui il percorso di traduzione di ExecuteUpdate per determinare perché la tabella derivata fornisce la colonna aggiornata; il lavoro è completato quando l’espressione SET fa riferimento all’alias della destinazione dell’aggiornamento e gli incrementi concorrenti preservano il totale combinato.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

area-bulkupdate customer-reported
Bug description

ExecuteUpdate over a projection can generate SQL where an increment expression uses a value read into a derived table instead of referencing the actual update target alias.

This can cause lost updates under concurrent executions.

The LINQ expression represents an atomic increment:

target.TotalQuantity = target.TotalQuantity + delta

But the generated SQL uses:

SET "TotalQuantity" = s2."TotalQuantity" + s2."QuantityDelta"

where s2."TotalQuantity" is read inside a derived table before the update.

For concurrent counter updates, the generated SQL should reference the update target alias:

SET "TotalQuantity" = s1."TotalQuantity" + s2."QuantityDelta"

because s1 is the actual row being updated.

Example lost-update scenario:

Initial TotalQuantity = 100

Transaction 1 delta = 10
Transaction 2 delta = 20

Both transactions can read s2.TotalQuantity = 100.

Transaction 1 updates row to 110.
Transaction 2 waits for the same row, then updates row to 100 + 20 = 120.

Expected final value: 130.
Possible final value: 120.
Your code
await context.ShortageOrderLines
    .Where(line => line.ShortageOrderId == orderId)
    .GroupBy(line => line.ShortageArticleTask!)
    .Select(g => new
    {
        Task = g.Key,
        QuantityDelta = g.Sum(l => l.Quantity)
    })
    .ExecuteUpdateAsync(setters => setters.SetProperty(
        t => t.Task.TotalQuantity,
        t => t.Task.TotalQuantity + t.QuantityDelta));

Generated SQL:

UPDATE shortage."ShortageArticleTasks" AS s1
SET "TotalQuantity" = s2."TotalQuantity" + s2."QuantityDelta"
FROM (
    SELECT s0."Id", s0."TotalQuantity", COALESCE(sum(s."Quantity"), 0.0) AS "QuantityDelta"
    FROM shortage."ShortageOrderLines" AS s
    LEFT JOIN shortage."ShortageArticleTasks" AS s0 ON s."ShortageArticleTaskId" = s0."Id"
    WHERE s."ShortageOrderId" = @orderId
    GROUP BY s0."Id", s0."ArticleId", s0."LogisticVariantId", s0."TotalQuantity"
) AS s2
WHERE s1."Id" = s2."Id"

Expected SQL shape:

UPDATE shortage."ShortageArticleTasks" AS s1
SET "TotalQuantity" = s1."TotalQuantity" + s2."QuantityDelta"
FROM (
    SELECT
        s."ShortageArticleTaskId",
        COALESCE(sum(s."Quantity"), 0.0) AS "QuantityDelta"
    FROM shortage."ShortageOrderLines" AS s
    WHERE s."ShortageOrderId" = @orderId
    GROUP BY s."ShortageArticleTaskId"
) AS s2
WHERE s1."Id" = s2."ShortageArticleTaskId"
Stack traces

No exception is thrown. The generated SQL executes successfully, but it may have incorrect concurrency semantics for counter increments.

EF Core version

10.0.8

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

net10.0

Operating system

Amazon Linux

IDE

VS 2026

Lingua principale
C#
Stelle
14.8k
Fork
3.4k
Merge medio
3g 18h
PR unite (30g)
159

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/efcore

Tutte le issue di dotnet/efcore

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.