I'm trying to setup optimistic concurrency in my application using RowVersion
property that is added to my entity.
I can't get the DbUpdateConcurrencyException to appear during testing.
The test that I have done is run an update SQL command on the entity while on the front end have the same entity loaded into memory.
Does anyone have any ideas where I'm going wrong?
Test SQL script:
UPDATE [dbo].[MyEntity] SET [Name] = 'fdgopiifdgdf' WHERE [Id] = '11111111-DF4E-4F24-B321-11111111'
Entity:
[Table("MyEntity")]
public class MyEntity : FullAuditedEntity, IConcurrencyCheck
{
[Required]
public virtual string Name { get; set; }
public virtual int Order { get; set; }
public virtual bool Active { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
Dto:
public class CreateOrEditMyEntityDto : EntityDto<Guid?>
{
[Required]
public string Name { get; set; }
public int Order { get; set; }
public bool Active { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
CustomDtoMapper for entity:
configuration.CreateMap<MyEntity, MyEntityDto>();
DbContext OnModelCreating method:
modelBuilder.Entity<MyEntity>()
.Property(a => a.RowVersion)
.IsRowVersion()
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();
Edit entity method:
public async Task Update(CreateOrEditMyEntityDto input)
{
// Handle multi processing concurrency issues.
try
{
var MyEntity = await _MyEntityRepository.FirstOrDefaultAsync((Guid)input.Id);
ObjectMapper.Map(input, MyEntity);
}
catch (DbUpdateConcurrencyException)
{
throw new UserFriendlyException("Please reload to edit this record.");
}
}
12 Answer(s)
-
0
ABP catches
DbUpdateConcurrencyException
and throwsAbpDbConcurrencyException
for EF Core.https://github.com/aspnetboilerplate/aspnetboilerplate/commit/9e6a6bfba161ea9d97fe6d7d4f52d890a11def90
-
0
I still can't get project to throw DbUpdateConcurrencyException inside the ASP.NET Zero project. Any ideas where I'm going wrong?
My update method looks like this:
private async Task Update(CreateOrEditAccountStatusDto input) { var accountStatus = await _accountStatusRepository.FirstOrDefaultAsync((Guid)input.Id); ObjectMapper.Map(input, accountStatus); }
DbContext looks like this
using MyProject.DevExpressDashboard; using MyProject.Wellspect; using MyProject.CustomerDetail; using MyProject.Reporting; using Abp.IdentityServer4; using Abp.Zero.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using MyProject.Authorization.Roles; using MyProject.Authorization.Users; using MyProject.Chat; using MyProject.Editions; using MyProject.Friendships; using MyProject.MultiTenancy; using MyProject.MultiTenancy.Accounting; using MyProject.MultiTenancy.Payments; using MyProject.Storage; using System.Linq; using System.Threading.Tasks; using System.Threading; using Abp.Domain.Uow; using Abp.UI; using System; namespace MyProject.EntityFrameworkCore { public class MyProjectDbContext : AbpZeroDbContext<Tenant, Role, User, MyProjectDbContext>, IAbpPersistedGrantDbContext { public virtual DbSet<EntityHistory> EntityHistory { get; set; } public virtual DbSet<AccountStatus> AccountStatuses { get; set; } public virtual DbSet<BinaryObject> BinaryObjects { get; set; } public virtual DbSet<ChatMessage> ChatMessages { get; set; } public virtual DbSet<SubscriptionPayment> SubscriptionPayments { get; set; } public MyProjectDbContext(DbContextOptions<MyProjectDbContext> options) : base(options) { } public override int SaveChanges() { try { return base.SaveChanges(); } catch (DbUpdateConcurrencyException innerException) { throw new UserFriendlyException("Concurrency exception!", innerException.Message, innerException); } } public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { try { return await base.SaveChangesAsync(cancellationToken); } catch (DbUpdateConcurrencyException ex) { var entity = ex.Entries.Single().GetDatabaseValues(); if (entity == null) { throw new UserFriendlyException("The entity being updated is already deleted by another user."); } else { throw new UserFriendlyException("The entity being updated has already been updated by another user."); } } } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // modelBuilder.Entity<EntityHistory>(E => // { // E.HasIndex(e => new { e.TenantId }); // }); modelBuilder.Entity<BinaryObject>(b => { b.HasIndex(e => new { e.TenantId }); }); modelBuilder.Entity<ChatMessage>(b => { b.HasIndex(e => new { e.TenantId, e.UserId, e.ReadState }); b.HasIndex(e => new { e.TenantId, e.TargetUserId, e.ReadState }); b.HasIndex(e => new { e.TargetTenantId, e.TargetUserId, e.ReadState }); b.HasIndex(e => new { e.TargetTenantId, e.UserId, e.ReadState }); }); modelBuilder.Entity<Friendship>(b => { b.HasIndex(e => new { e.TenantId, e.UserId }); b.HasIndex(e => new { e.TenantId, e.FriendUserId }); b.HasIndex(e => new { e.FriendTenantId, e.UserId }); b.HasIndex(e => new { e.FriendTenantId, e.FriendUserId }); }); modelBuilder.Entity<Tenant>(b => { b.HasIndex(e => new { e.SubscriptionEndDateUtc }); b.HasIndex(e => new { e.CreationTime }); }); modelBuilder.Entity<SubscriptionPayment>(b => { b.HasIndex(e => new { e.Status, e.CreationTime }); b.HasIndex(e => new { e.PaymentId, e.Gateway }); }); modelBuilder.Entity<AccountStatus>() .Property(a => a.RowVersion) .IsRowVersion(); } } }
-
0
@aaron, your reference to https://github.com/aspnetboilerplate/aspnetboilerplate/commit/9e6a6bfba161ea9d97fe6d7d4f52d890a11def90
on ABP catches DbUpdateConcurrencyException and throws AbpDbConcurrencyException for EF Core and this is NOT implementated for the Standard EF right?
-
0
For EF6,
DbEntityValidationException
will be thrown instead.See https://github.com/aspnetboilerplate/aspnetboilerplate/blob/b9326bc79d08998a6148032a9302d11959464191/src/Abp.EntityFramework/EntityFramework/AbpDbContext.cs#L213-L224
-
0
Got it working I adjusted how my simulated testing was done. Reference from https://docs.microsoft.com/en-us/ef/ef6/saving/concurrency
-
0
Hi there I'm trying to simulate concurrency using the two tab approach e.g.
- Tab 1 data loaded into memory and tab 2 data loaded into memory.
- Make changes to tab 1 then save.
- Make changes to tab 2 then save (shouldn't let you because the RowVersion should be different).
Simulating concurrency: The only way that I can get the simulating to work is by setting a breakpoint on the ObjectMapper.Map(input, accountStatus); and then modifying the entity that is being saved in the database using another tool such as SQL Management Studio running an Update SQL script.
Update method:
private async Task Update(CreateOrEditAccountStatusDto input) { var accountStatus = await _accountStatusRepository.FirstOrDefaultAsync((Guid)input.Id); ObjectMapper.Map(input, accountStatus); }
Current status: At the moment the only way I can get the two tab approach test case to work is by manually doing a RowVersion comparison inside the entity then throwing an exception if the byte arrays don't match. I want to handle the comparison at entityframework level so I don't have to make a code change to each entity. Please let me know where I'm going wrong.
Issue: Why doesn't the DbUpdateConcurrencyException get thrown on the two tab approach?
-
0
Your question title and code says EF Core.
-
0
@aaron it's using EntityframeworkCore as the title says.
-
0
Article for EF Core: https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/concurrency?view=aspnetcore-2.0
A simple way to handle that in your DbContext with ABP v2.3.0+, adapted from EF6 answer in #4146:
protected override void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, long? userId, EntityChangeReport changeReport) { if (entry.Entity is MyRowVersionEntity) { entry.OriginalValues["RowVersion"] = entry.CurrentValues["RowVersion"]; } base.ApplyAbpConceptsForModifiedEntity(entry, userId, changeReport); }
-
0
@aaron is there a way of making MyRowVersionEntity more generic so I don't need to specify each entity?
-
0
Yes. You can define an interface, e.g.
IHasConcurrency
as suggested in aspnetboilerplate/aspnetboilerplate#50. -
0
Cheers