Handling data concurrency in EF Core and ASP.NET Core WebAPI
Image from Unsplash

Handling data concurrency in EF Core and ASP.NET Core WebAPI

Data concurrency in Entity Framework Core

Entity Framework Core implements optimistic concurrency strategy which is opposite to pessimistic concurrency strategy. Lets's first clear out what are main differences between these two approaches in concurrency control.

Optimistic concurrency control allows multiple processes to access and modify data without any explicit lock on the data. This increases performances significantly but opens the door to possible data concurrency which may cause data inconsistency and data overwriting. In most of the cases you will not have to deal with concurrency issues. It is a part of good practice and something you need to implement to avoid these corner case situations.

Pessimistic concurrency control performs explicit lock on the record that is edited and any other process that wants to edit the same record has to wait for the process that locked it to release it. This approach is not so popular and used because it can make things complicated. You cannot keep the record locked for ever, so some lock timeout mechanism has to be in place and other things that initially impact performance downgrade as well as issues

Since EF Core implements optimistic concurrency control we will focus on that without going into the pessimistic concurrency control. 

To enable optimistic concurrency control in EF Core you need to declare one of your entity properties as concurrency token with ConcurrencyCheck attribute. This property is marked as a Timestamp field and it will be returned as byte array value once mapped from the table. Since we will probably use this control for more than one table in our solution, it is a good practice to declare this property along with primary key in abstract class which will be inherited by every entity in your application.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Sample.Concurrency.Data.Entities
{
    public abstract class BaseEntity
    {
        [Key, Column(Order = 0)]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }

        [ConcurrencyCheck]
        [Timestamp]
        public byte[] Version { get; set; }
    }
}
    

All the classes that inherit BaseEntity will have the primary key Id GUID filed and Version field as concurrency token which will be checked on entity update. If the version of the entity does not match on saving dbContext, EF will throw exception of type DbUpdateConcurrencyException.

Timestamp will ensure that new value is generated and stored in the filed when record is created or updated. This way you can always compare only this field for the consistency instead of having to check all the properties of the entity for changes. ConcurrencyCheck attribute will tell EF Core to check and compare values of the field to determine if there is a concurrency conflict.

Now for the test purpose I created on sample entity which will inherit the base entity abstract class. 

using System;

namespace Sample.Concurrency.Data.Entities
{
    public class Product:BaseEntity
    {
        public String Name { get; set; }
        public String Description { get; set; }
        public double Price { get; set; }
    }
}

    

This is pretty much you have to do to ensure data overwriting from different users of your application. EF Core will simply throw exception if concurrency happens. Now you also need to ensure that Version field value is transfered across all layers in your application.

Typically you will use AutoMapper map your entity to a DTO model in ASP.NET MVC WebAPI and from DTO to entity when data is submitted. Since our Version field is of byte array type, you will have to serialize it when sending to facade Web API layer and deserialize when sending data from WebAPI to your EF Core data library. I found base64 the most suitable way of doing this, so we eventually end up with two mappings for this purpose. One mapping maps entity to DTO and serializes version to Base64 while the other mapping declared is doing the opposite, mapping DTO to entity and deserializing Version field from Base64 string to byte array.

Note

Version field needs to present in the edit DTO every time on both PATCH and GET so that EF Core can compare the value when updating the entity. If not present, EF Core will try to math empty value with the one present in database and you will not be able to update the entity because of DbUpdateConcurrencyException thwoen every time you try to update with empty Version field

using AutoMapper;
using Sample.Concurrency.Data.Entities;
using Sample.Concurrency.Web.Api.Models;
using System;

namespace Sample.Concurrency.Web.Api.Mapping
{
    public class ProductMapper : Profile
    {
        public ProductMapper()
        {
            CreateMap<ProductEditModel, Product>()
                .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
                .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
                .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price))
                .ForMember(dest => dest.Version, opt => opt.MapFrom(src => Convert.FromBase64String(src.Version)));

            CreateMap<Product, ProductEditModel>()
                .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
                .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
                .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price))
                .ForMember(dest => dest.Version, opt => opt.MapFrom(src => Convert.ToBase64String(src.Version)));
        }
    }
}
    

Now to make sure that your client knows that there has been a conflict during data saving, you need to catch DbUpdateConcurrencyException exception and response with a proper HTTP status code. You can return 400 Bad request but I prefer to return 409 Confilct as it is more descriptive because reason for the failure is data conflict. Since I am using UnitOfWork instance for accessing DbContext and entities, I just wrapped Save method in try/catch where I am catching only exceptions of type DbUpdateConcurrencyException 

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Sample.Concurrency.Data.Entities;
using Sample.Concurrency.Data.UnitsOfWork;
using Sample.Concurrency.Web.Api.Models;

namespace Sample.Concurrency.Web.Api.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : BaseController
    {
        public ProductsController(IUnitOfWork unitOfWork,IMapper mapper) : base(unitOfWork,mapper)
        {
        }
        ...
        [HttpGet("{id}", Name = "Product")]
        public async Task<ProductEditModel> Get(Guid id)
        {
            return this.mapper.Map<ProductEditModel>(await unitOfWork.Products.GetByIdAsync(id));
        }
		...
        [HttpPatch("{id}")]
        public async Task<IActionResult> Patch(int id, [FromBody] ProductEditModel product)
        {
            await this.unitOfWork.Products.InsertAsync(this.mapper.Map<Product>(product));
            try
            {
                await this.unitOfWork.SaveAsync();
            }
            catch (DbUpdateConcurrencyException ex)
            {
                return Conflict(ex);
            }
            return Ok();
        }
		...
    }
}

    

Resolving the data concurrency conflict

In he example above, we are not resolving the conflict programmatic in our code. Instead we are telling the client that there is a conflict on saving and that client is responsible for resolving this, either by prompting user to reload the data from the database or to cancel entity update.

Concurrency conflict can be also resolved in your code if you decide to use certain policy when these kind of exceptions occur. Microsoft documentation describes this approach nice in the article Handling Concurrency Conflicts, so I will not try to explain it here as the resolution strategy may vary from application to application. The safest way is to leave it to the client to resolve the conflict if you do not have all the cases for every field and field combination that can occur in conflict.

Snippets from the sample above are just part of the sample project you can find on github https://github.com/dejanstojanovic/dotnetcore-efcore-data-concurrency and see how the whole solution is organized with all references and configurations. 

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article