dotnet / dotnet/efcore

DbUpdateException Entries Inconsistent When First Fails (i = 0) vs Subsequent Fails (i > 0)

Open
#33,098 0 comments 0 reactions 0 assignees View on GitHub
area-save-changes customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

When saving multiple instances of an entity and the first one has an error, DbUpdateException.Entries has all the dirty entities from the batch. However, when any others has an error, DbUpdateException.Entries only has the first one that has an error. Could be the second or the 50th, but there is only one.

It's because ExecuteReader handles exceptions differently than Consume.

ExecuteReader selects all, but Consume only picks the first one that failed. They should be consistent. Only the first one should be there when ExecuteReader - similar logic to Consume. ExecuteReader will return a DbReader that has read the first record and threw an exception if that one had an error. It does not wait for Consume. This is where EF can help by being consistent between the two.

It would also be helpful to document that it will only set the first one that failed, not all in the batch. What would be even better is if it actually checked all in the batch and only set the ones that failed. Current documentation is extremely vague.

see code here: https://github.com/dotnet/efcore/blob/baae39267197866446da1e9533487d18d7bcd317/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs#L358

and here: https://github.com/dotnet/efcore/blob/42e6cfbd0c5b431a89b1923e7b73296705cf7ddf/src/EFCore.Relational/Update/AffectedCountModificationCommandBatch.cs#L139

Async versions as well.

### runnable sample code

```C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

/*
-- run the following on your db to create user and grant priveleges
-- or update connstr in PersonDbContext to use what you like
CREATE USER 'user'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON poi.* TO 'user'@'%';
*/

using var ctx = new PersonDbContext();
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();

await TrySaveChanges(context =>
// init with some records
context.People.AddRange(new []
{
new Person { Name = "Thr" },
new Person { Name = "Two" },
new Person { Name = "One" },
new Person { Name = "Zer" }
})
);

await TrySaveChanges(context => {
// update first record with name.length > max for field
// Entries has all 4 updated Entities - regardless of whether they failed or not
var people = context.People.ToArray();
foreach(var p in people)
p.Name = "OK";
people[0].Name = "NOT EXPECTED RESULT";
});

await TrySaveChanges(context => {
// update second record with name.length > max for field
// Entries has only 1 updated Entity 0 the first one that failed
var people = context.People.ToArray();
foreach(var p in people)
p.Name = "OK";
people[1].Name = "EXPECTED RESULT";
});

ctx.Database.EnsureDeleted();

static async Task TrySaveChanges(Action act)
{
using var context = new PersonDbContext();
try
{
act(context);
await context.SaveChangesAsync();
Console.WriteLine("Saved");
}
catch (DbUpdateException ex)
{
if (ex.Entries.Count != 1) Console.WriteLine("Unexpected number of Entries");
else if (ex.Entries.Count == 1) Console.WriteLine("Expected number of Entries");
else Console.WriteLine(@"¯\_(ツ)_/¯");
}
}

class PersonDbContext : DbContext
{
public virtual DbSet People { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connstr = "server=localhost;port=3306;database=poi;user=user;password=password;";
optionsBuilder.UseMySql(
connstr,
ServerVersion.AutoDetect(connstr)
);
base.OnConfiguring(optionsBuilder);
}
}

[Table("Persons")]
public class Person
{
[Key]
public int Id { get; set; }

[MaxLength(3)]
public string Name { get; set; } = string.Empty;
}

```

```XML


Exe
net6.0
enable
enable



```

### version info - but not relevant

All are irrelevant to the issue since the EF Core code is the same.

EF Core version: 6.0.0
Database provider: Pomelo.EntityFrameworkCore.MySql
Target framework: .NET 6.0
Operating system: (irrelevant)
IDE: (irrelevant)
MySQL: 5.7

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.