Re: MySql.EntityFrameworkCore No DbUpdateConcurrencyException thrown when concurrency conflictis expected
Posted by: Karl Walker
Date: July 30, 2024 02:38AM
Date: July 30, 2024 02:38AM
I have the same problem, the problem started when `MySql.EntityFramework` migrated from v6 to v7, and still is present in v8.
Here are some examples...
Base Entity...
```
public abstract class BaseEntity
{
public Guid ID { get; set; } = Guid.Empty; // primary key
[AllowInsertPreventUpdate]
public DateTime CreatedDate { get; set; } = DateTime.MinValue;
public DateTime ModifiedDate { get; set; } = DateTime.MinValue;
public bool IsDeleted { get; set; } = false;
public Guid RowVersion { get; set; } = Guid.Empty; // concurrency token
}
```
Base Entity Configuration...
```
internal abstract class BaseEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
where TEntity : BaseEntity
{
protected string TableName { get; }
protected BaseEntityConfiguration(string tableName)
{
TableName = tableName;
}
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
// table
builder.ToTable(TableName);
builder.ForMySQLHasCollation(Constants.Database.StringCollation);
builder.HasAnnotation("MySql:CharSet", Constants.Database.CharSet);
// indexes
builder.HasIndex(e => e.IsDeleted, "IX_IsDeleted").HasDatabaseName("IX_IsDeleted");
// pk
builder.HasKey(e => e.ID);
builder.Property(e => e.ID)
.IsRequired()
.HasColumnType("char(36)")
.ForMySQLHasCollation(Constants.Database.BinaryCollation)
.HasDefaultValueSql("''") // mandate default due to default behaviour in EF 6.44
.ValueGeneratedNever();
// meta
builder.Property(e => e.CreatedDate)
.HasColumnType("datetime");
builder.Property(e => e.ModifiedDate)
.HasColumnType("datetime");
builder.Property(e => e.IsDeleted)
.IsRequired()
.HasColumnType("bit(1)");
builder.Property(e => e.RowVersion)
.IsRequired()
.HasColumnType("char(36)")
.ForMySQLHasCollation(Constants.Database.BinaryCollation)
.HasDefaultValueSql("''")
.IsConcurrencyToken();
}
}
```
SaveChangesAsync() Override...
```
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var concurrencyTokens = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in concurrencyTokens.Where(t => t.State != EntityState.Unchanged))
{
// set new version for concurrency checks
entry.Entity.RowVersion = Guid.NewGuid();
// update created and modified dates
var dateAndTime = DateTime.UtcNow;
if (entry.State == EntityState.Added)
{
var addedTime = DateTime.UtcNow;
entry.Entity.CreatedDate =
entry.Entity.CreatedDate == DateTime.MinValue
? dateAndTime
: entry.Entity.CreatedDate; // override if set
entry.Entity.ModifiedDate = dateAndTime;
}
else
{
// set update date
entry.Entity.ModifiedDate = dateAndTime;
}
}
if (!AreImmutableFieldsTurnedOff())
{
PreventUnwantedPropertiesFromBeingUpdated();
}
return await base.SaveChangesAsync(cancellationToken);
}
```
Unit Test
```
[Test]
public async Task Update_Record_Returns_Concurrency_Exception()
{
// arrange
var territoryTypeToCreate = new TerritoryType
{
Name = "Test Save",
Territories = null
};
// act
var createState = await _territoryTypeRepository.CreateAsync(null, territoryTypeToCreate);
if (createState.Model is null)
{
Assert.Fail();
return;
}
// User 1, then
var territoryTypeUser1 = await _territoryTypeRepository.GetAsync(null, createState.Model.ID, useWriter: true);
if (territoryTypeUser1 is null)
{
Assert.Fail();
return;
}
// User 2, then
var territoryTypeUser2 = await _territoryTypeRepository.GetAsync(null, createState.Model.ID, useWriter: true);
if (territoryTypeUser2 is null)
{
Assert.Fail();
return;
}
// update User 1
territoryTypeUser1.Name = "Changed Territory Type Initial";
var update1State = await _territoryTypeRepository.UpdateAsync(null, territoryTypeUser1);
if (update1State.Model is null)
{
Assert.Fail();
return;
}
// update User 2
territoryTypeUser2.Name = "Changed Territory Type Again";
var update2State = await _territoryTypeRepository.UpdateAsync(null, territoryTypeUser2);
if (update2State.Model is null)
{
Assert.Fail();
return;
}
// assert
Assert.That(createState, Is.Not.Null);
Assert.That(createState.IsCreated, Is.EqualTo(true));
Assert.That(createState.Errors.Count, Is.EqualTo(0));
Assert.That(createState.IsException, Is.EqualTo(false));
Assert.That(createState.Model, Is.Not.Null);
// --> read 1 & 2
Assert.That(territoryTypeUser1, Is.Not.Null);
Assert.That(territoryTypeUser2, Is.Not.Null);
// -> update 1
Assert.That(update1State, Is.Not.Null);
Assert.That(update1State.IsUpdated, Is.EqualTo(true));
Assert.That(update1State.Errors.Count, Is.EqualTo(0));
Assert.That(update1State.IsException, Is.EqualTo(false));
Assert.That(update1State.Model, Is.Not.Null);
Assert.That(update1State.IsConcurrency, Is.EqualTo(false));
// --> update 2
Assert.That(update2State, Is.Not.Null);
Assert.That(update2State.IsUpdated, Is.EqualTo(false));
Assert.That(update2State.Errors.Count, Is.GreaterThan(0));
Assert.That(update2State.IsException, Is.EqualTo(false));
Assert.That(update2State.IsConcurrency, Is.EqualTo(true));
}
```
Update State...
```
public abstract class BaseState<TEntity>
{
public IDictionary<string, string> Errors { get; set; } = new Dictionary<string, string>();
public TEntity? Model { get; set; } = default;
public bool IsException { get; set; } = false;
public bool IsFatal { get; set; } = false;
public bool IsConcurrency { get; set; } = false;
}
public class UpdateState<TEntity> : BaseState<TEntity>
{
public bool IsUpdated { get; set; } = false;
}
```
Repo Update Call...
```
public async Task<UpdateState<TEntity>> UpdateAsync(
ITransaction? transaction,
TEntity entity)
{
var ct = new CancellationToken();
if (transaction is not null)
{
var typedDatabaseTransaction = transaction as Transaction<TContext>;
if (typedDatabaseTransaction == null)
throw new ArgumentException("Invalid or null database transaction type");
return await UpdateAsync(typedDatabaseTransaction.Context, entity, ct);
}
await using var context = ContextFactory.CreateContext(useReader: false);
return await UpdateAsync(context, entity, ct);
}
protected virtual async Task<UpdateState<TEntity>> UpdateAsync(
TContext context,
TEntity entity,
CancellationToken cancellationToken)
{
try
{
if (entity.ID == Guid.Empty)
{
throw new ApplicationException($"Unable to update item, ID has a value of {entity.ID}");
}
context.Entry(entity).State = EntityState.Modified;
var changes = await context.SaveChangesAsync(cancellationToken);
return new UpdateState<TEntity>
{
IsUpdated = true,
Model = entity
};
}
catch (DbUpdateConcurrencyException cex)
{
// concurrency exception
var (diffs, clientEntity) = await HandleConcurrencyAsync(cex);
return new UpdateState<TEntity>
{
IsUpdated = false,
Errors = diffs,
Model = clientEntity,
IsException = false,
IsConcurrency = true
};
}
catch (Exception e)
{
// return failure
return new UpdateState<TEntity>
{
IsUpdated = false,
Errors = new Dictionary<string, string> { { "", e.Message } },
Model = entity,
IsException = true,
IsConcurrency = false
};
}
}
```
DbUpdateConcurrencyException is NEVER HIT...
SQL Ran via logging is...
```
UPDATE `TerritoryType`
SET `CreatedDate` = '2024-07-29T14:06:52',
`IsDeleted` = 0,
`ModifiedDate` = '2024-07-29T14:10:51',
`Name` = 'Changed Territory Type Again',
`RowVersion` = 'dd7ffd0c-50ac-45a8-978f-88a4aa11ce47'
WHERE `ID` = '3a1410fd-984c-8bb7-f28c-a917634e7b68'
AND `RowVersion` = '11010ca6-b2d2-47ef-b1e9-9d5846a44b12';
```
IF I run this direct against SQL I get 0 rows changed.
When I run `SaveChangesAsync()` the value of ` is returned, but not row is updated.
No `DbUpdateConcurrencyException` is ever caught.
Please can you fix this please? We can't move off EFCore 6 until
Here are some examples...
Base Entity...
```
public abstract class BaseEntity
{
public Guid ID { get; set; } = Guid.Empty; // primary key
[AllowInsertPreventUpdate]
public DateTime CreatedDate { get; set; } = DateTime.MinValue;
public DateTime ModifiedDate { get; set; } = DateTime.MinValue;
public bool IsDeleted { get; set; } = false;
public Guid RowVersion { get; set; } = Guid.Empty; // concurrency token
}
```
Base Entity Configuration...
```
internal abstract class BaseEntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
where TEntity : BaseEntity
{
protected string TableName { get; }
protected BaseEntityConfiguration(string tableName)
{
TableName = tableName;
}
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
// table
builder.ToTable(TableName);
builder.ForMySQLHasCollation(Constants.Database.StringCollation);
builder.HasAnnotation("MySql:CharSet", Constants.Database.CharSet);
// indexes
builder.HasIndex(e => e.IsDeleted, "IX_IsDeleted").HasDatabaseName("IX_IsDeleted");
// pk
builder.HasKey(e => e.ID);
builder.Property(e => e.ID)
.IsRequired()
.HasColumnType("char(36)")
.ForMySQLHasCollation(Constants.Database.BinaryCollation)
.HasDefaultValueSql("''") // mandate default due to default behaviour in EF 6.44
.ValueGeneratedNever();
// meta
builder.Property(e => e.CreatedDate)
.HasColumnType("datetime");
builder.Property(e => e.ModifiedDate)
.HasColumnType("datetime");
builder.Property(e => e.IsDeleted)
.IsRequired()
.HasColumnType("bit(1)");
builder.Property(e => e.RowVersion)
.IsRequired()
.HasColumnType("char(36)")
.ForMySQLHasCollation(Constants.Database.BinaryCollation)
.HasDefaultValueSql("''")
.IsConcurrencyToken();
}
}
```
SaveChangesAsync() Override...
```
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var concurrencyTokens = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in concurrencyTokens.Where(t => t.State != EntityState.Unchanged))
{
// set new version for concurrency checks
entry.Entity.RowVersion = Guid.NewGuid();
// update created and modified dates
var dateAndTime = DateTime.UtcNow;
if (entry.State == EntityState.Added)
{
var addedTime = DateTime.UtcNow;
entry.Entity.CreatedDate =
entry.Entity.CreatedDate == DateTime.MinValue
? dateAndTime
: entry.Entity.CreatedDate; // override if set
entry.Entity.ModifiedDate = dateAndTime;
}
else
{
// set update date
entry.Entity.ModifiedDate = dateAndTime;
}
}
if (!AreImmutableFieldsTurnedOff())
{
PreventUnwantedPropertiesFromBeingUpdated();
}
return await base.SaveChangesAsync(cancellationToken);
}
```
Unit Test
```
[Test]
public async Task Update_Record_Returns_Concurrency_Exception()
{
// arrange
var territoryTypeToCreate = new TerritoryType
{
Name = "Test Save",
Territories = null
};
// act
var createState = await _territoryTypeRepository.CreateAsync(null, territoryTypeToCreate);
if (createState.Model is null)
{
Assert.Fail();
return;
}
// User 1, then
var territoryTypeUser1 = await _territoryTypeRepository.GetAsync(null, createState.Model.ID, useWriter: true);
if (territoryTypeUser1 is null)
{
Assert.Fail();
return;
}
// User 2, then
var territoryTypeUser2 = await _territoryTypeRepository.GetAsync(null, createState.Model.ID, useWriter: true);
if (territoryTypeUser2 is null)
{
Assert.Fail();
return;
}
// update User 1
territoryTypeUser1.Name = "Changed Territory Type Initial";
var update1State = await _territoryTypeRepository.UpdateAsync(null, territoryTypeUser1);
if (update1State.Model is null)
{
Assert.Fail();
return;
}
// update User 2
territoryTypeUser2.Name = "Changed Territory Type Again";
var update2State = await _territoryTypeRepository.UpdateAsync(null, territoryTypeUser2);
if (update2State.Model is null)
{
Assert.Fail();
return;
}
// assert
Assert.That(createState, Is.Not.Null);
Assert.That(createState.IsCreated, Is.EqualTo(true));
Assert.That(createState.Errors.Count, Is.EqualTo(0));
Assert.That(createState.IsException, Is.EqualTo(false));
Assert.That(createState.Model, Is.Not.Null);
// --> read 1 & 2
Assert.That(territoryTypeUser1, Is.Not.Null);
Assert.That(territoryTypeUser2, Is.Not.Null);
// -> update 1
Assert.That(update1State, Is.Not.Null);
Assert.That(update1State.IsUpdated, Is.EqualTo(true));
Assert.That(update1State.Errors.Count, Is.EqualTo(0));
Assert.That(update1State.IsException, Is.EqualTo(false));
Assert.That(update1State.Model, Is.Not.Null);
Assert.That(update1State.IsConcurrency, Is.EqualTo(false));
// --> update 2
Assert.That(update2State, Is.Not.Null);
Assert.That(update2State.IsUpdated, Is.EqualTo(false));
Assert.That(update2State.Errors.Count, Is.GreaterThan(0));
Assert.That(update2State.IsException, Is.EqualTo(false));
Assert.That(update2State.IsConcurrency, Is.EqualTo(true));
}
```
Update State...
```
public abstract class BaseState<TEntity>
{
public IDictionary<string, string> Errors { get; set; } = new Dictionary<string, string>();
public TEntity? Model { get; set; } = default;
public bool IsException { get; set; } = false;
public bool IsFatal { get; set; } = false;
public bool IsConcurrency { get; set; } = false;
}
public class UpdateState<TEntity> : BaseState<TEntity>
{
public bool IsUpdated { get; set; } = false;
}
```
Repo Update Call...
```
public async Task<UpdateState<TEntity>> UpdateAsync(
ITransaction? transaction,
TEntity entity)
{
var ct = new CancellationToken();
if (transaction is not null)
{
var typedDatabaseTransaction = transaction as Transaction<TContext>;
if (typedDatabaseTransaction == null)
throw new ArgumentException("Invalid or null database transaction type");
return await UpdateAsync(typedDatabaseTransaction.Context, entity, ct);
}
await using var context = ContextFactory.CreateContext(useReader: false);
return await UpdateAsync(context, entity, ct);
}
protected virtual async Task<UpdateState<TEntity>> UpdateAsync(
TContext context,
TEntity entity,
CancellationToken cancellationToken)
{
try
{
if (entity.ID == Guid.Empty)
{
throw new ApplicationException($"Unable to update item, ID has a value of {entity.ID}");
}
context.Entry(entity).State = EntityState.Modified;
var changes = await context.SaveChangesAsync(cancellationToken);
return new UpdateState<TEntity>
{
IsUpdated = true,
Model = entity
};
}
catch (DbUpdateConcurrencyException cex)
{
// concurrency exception
var (diffs, clientEntity) = await HandleConcurrencyAsync(cex);
return new UpdateState<TEntity>
{
IsUpdated = false,
Errors = diffs,
Model = clientEntity,
IsException = false,
IsConcurrency = true
};
}
catch (Exception e)
{
// return failure
return new UpdateState<TEntity>
{
IsUpdated = false,
Errors = new Dictionary<string, string> { { "", e.Message } },
Model = entity,
IsException = true,
IsConcurrency = false
};
}
}
```
DbUpdateConcurrencyException is NEVER HIT...
SQL Ran via logging is...
```
UPDATE `TerritoryType`
SET `CreatedDate` = '2024-07-29T14:06:52',
`IsDeleted` = 0,
`ModifiedDate` = '2024-07-29T14:10:51',
`Name` = 'Changed Territory Type Again',
`RowVersion` = 'dd7ffd0c-50ac-45a8-978f-88a4aa11ce47'
WHERE `ID` = '3a1410fd-984c-8bb7-f28c-a917634e7b68'
AND `RowVersion` = '11010ca6-b2d2-47ef-b1e9-9d5846a44b12';
```
IF I run this direct against SQL I get 0 rows changed.
When I run `SaveChangesAsync()` the value of ` is returned, but not row is updated.
No `DbUpdateConcurrencyException` is ever caught.
Please can you fix this please? We can't move off EFCore 6 until
Subject
Written By
Posted
April 10, 2024 08:05PM
Re: MySql.EntityFrameworkCore No DbUpdateConcurrencyException thrown when concurrency conflictis expected
July 30, 2024 02:38AM
Sorry, only registered users may post in this forum.
Content reproduced on this site is the property of the respective copyright holders. It is not reviewed in advance by Oracle and does not necessarily represent the opinion of Oracle or any other party.