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

Abierto
#38,505 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 2 días

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
52/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Tranquilo
Stack tecnológico
csharp, postgresql
Área
databases

Línea de trabajo

Comience reproduciendo la consulta LINQ ExecuteUpdateAsync proporcionada e inspeccionando el SQL de PostgreSQL generado. Siga la ruta de traducción de ExecuteUpdate para determinar por qué la tabla derivada proporciona la columna actualizada; se considera completado cuando la expresión SET hace referencia al alias del destino de actualización y los incrementos concurrentes conservan el total combinado.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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

Lenguaje dominante
C#
Estrellas
14.8k
Forks
3.4k
Merge medio
3 d 18 h
PR fusionados (30 d)
159

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de dotnet/efcore

Todos los issues de dotnet/efcore

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.