By default: last write wins.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // ====================================================================== // Q: Two users edit the same product at the same time. What happens and how do // you prevent it? // // By default: last write wins. The second user overwrites the first user's // change without knowing it (lost update). Solution: optimistic concurrency with // a concurrency token. // EF Core puts the token in the WHERE clause. If it changed in the meantime, 0 // rows are affected and it throws DbUpdateConcurrencyException. // In an API: the version goes to the client (ETag / version field) and comes // back. On conflict: 409 Conflict (or 412 with If-Match), and the user reloads. // In a background job: reload and retry (or merge per field). // ====================================================================== // SQL Server: rowversion that changes automatically on every UPDATE public class Product { public int Id { get; set; } public decimal ListPrice { get; set; } [Timestamp] public byte[] RowVersion { get; set; } = []; } // PostgreSQL (Npgsql): the xmin system column as the token, no extra column public uint Version { get; set; } modelBuilder.Entity<Product>().Property(p => p.Version).IsRowVersion(); // Or for specific fields only [ConcurrencyCheck] public decimal ListPrice { get; set; } // PUT /products/{sku}: the version the client saw becomes the "original value" var product = await db.Products.SingleAsync(p => p.Sku == sku, ct); db.Entry(product).Property(p => p.Version).OriginalValue = request.Version; product.ListPrice = request.Price; try { await db.SaveChangesAsync(ct); return Results.NoContent(); } catch (DbUpdateConcurrencyException) { return Results.Conflict("The product was changed by someone else. Reload and retry."); } // Background job: reload and retry on a concurrency conflict for (var attempt = 1; ; attempt++) { try { product.Stock += delta; await db.SaveChangesAsync(ct); break; } catch (DbUpdateConcurrencyException ex) when (attempt < 3) { var entry = ex.Entries.Single(); var current = await entry.GetDatabaseValuesAsync(ct) ?? throw new NotFoundException(); // deleted in the meantime entry.OriginalValues.SetValues(current); // new token entry.CurrentValues.SetValues(current); // "database wins", then we reapply } } |