Base solution for your next web application
Open Closed

How to handle concurrency in asp.net framework 4.6.1 using entityframework core. #6262


User avatar
0
system15 created

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)
  • User Avatar
    0
    aaron created
    Support Team

    ABP catches DbUpdateConcurrencyException and throws AbpDbConcurrencyException for EF Core.

    https://github.com/aspnetboilerplate/aspnetboilerplate/commit/9e6a6bfba161ea9d97fe6d7d4f52d890a11def90

  • User Avatar
    0
    system15 created

    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();
            }
        }
    }
    
  • User Avatar
    0
    tteoh created

    @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?

  • User Avatar
    0
    ryancyq created
    Support Team

    For EF6, DbEntityValidationException will be thrown instead.

    See https://github.com/aspnetboilerplate/aspnetboilerplate/blob/b9326bc79d08998a6148032a9302d11959464191/src/Abp.EntityFramework/EntityFramework/AbpDbContext.cs#L213-L224

  • User Avatar
    0
    system15 created

    Got it working I adjusted how my simulated testing was done. Reference from https://docs.microsoft.com/en-us/ef/ef6/saving/concurrency

  • User Avatar
    0
    system15 created

    Hi there I'm trying to simulate concurrency using the two tab approach e.g.

    1. Tab 1 data loaded into memory and tab 2 data loaded into memory.
    2. Make changes to tab 1 then save.
    3. 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?

  • User Avatar
    0
    aaron created
    Support Team

    Your question title and code says EF Core.

  • User Avatar
    0
    system15 created

    @aaron it's using EntityframeworkCore as the title says.

  • User Avatar
    0
    aaron created
    Support Team

    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);
    }
    
  • User Avatar
    0
    system15 created

    @aaron is there a way of making MyRowVersionEntity more generic so I don't need to specify each entity?

  • User Avatar
    0
    aaron created
    Support Team

    Yes. You can define an interface, e.g. IHasConcurrency as suggested in aspnetboilerplate/aspnetboilerplate#50.

  • User Avatar
    0
    system15 created

    Cheers