DbUpdateException Entries Inconsistent When First Fails (i = 0) vs Subsequent Fails (i > 0)
- 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
Assessment
This issue has not been assessed yet.