Search code examples
c#entity-framework-coreoptimistic-concurrency

Why RowVersion property is not raising optimistic concurrency exception when two concurrent changes occur?


I have the following entity,

 public class PatientRegistry : BaseEntity {

        [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Patient File Number")]
        public long PatientFileId { get; set; }
        public virtual ICollection<PartnerRegistry> Partners { get; set; }
        public string UserId { get; set; }
        public string AliasName { get; set; }

        public DateTime? DateNow { get; set; }
        public virtual ApplicationUser User { get; set; }

    }

and the BaseEntity is,

 public class BaseEntity : ISoftDelete {
        public bool IsDeleted { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }
    }

the entity has RowVersion property and I use the fluent API to configure it as RowVersion. I use the repository pattern and this is how I load the entity in the context,

 public async Task<PatientRegistry> GetPatient(long Id, bool isPartners = true, bool isUser = true) {
            if (isPartners && !isUser)
              return await context.PatientsRegistry
                .IgnoreQueryFilters()
                .Include(pt => pt.Partners)
                .Include(pt => pt.User)
                .Include(pt => pt.User.Gender)
                .Include(pt => pt.User.Nationality)
                .Include(pt => pt.User.Occupation)
                .Include(pt => pt.User.Ethnicity)
                .Include(pt => pt.User.MaritalStatus)
                .Include(pt => pt.User.Country)
                .Include(pt => pt.User.State)
                .Include(pt => pt.User.City)
                .Include(pt => pt.User.Database)
                .Include(pt => pt.User.UserAvatar)
                .SingleOrDefaultAsync(pt => pt.PatientFileId == Id);
        }

and in my controller, I update as following,

[HttpPut("{FileId}")]
public async Task<IActionResult> UpdatePatient([FromRoute] PatientFileIdResource fileIdModel, [FromBody] SavePatientsRegistryResource model)
{
    ApiSuccessResponder<PatientRegistryResource> response = new ApiSuccessResponder<PatientRegistryResource>();
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    Task<PatientRegistry> getPatientTask = repository.GetPatient(fileIdModel.FileId.Value);
    Task<RequestHeaderResource> getHeaderRequestTask = headerRequestService.GetUserHeaderData();
    await Task.WhenAll(getPatientTask, getHeaderRequestTask);
    PatientRegistry patient = await getPatientTask.ConfigureAwait(false);
    RequestHeaderResource header = await getHeaderRequestTask.ConfigureAwait(false);
    if (patient == null)
    {
        ModelState.AddModelError(string.Empty, "The patient does not exist in the database");
        return BadRequest(ModelState);
    }

    patient.DateNow = DateTime.Now;
    patient.AliasName = model.AliasName;
    await unitOfWork.CompleteAsync(header);
    return StatusCode(200, response);
}

Now if I access the same record with two different users and one of the alters AliasName and saves the second user that accessed the record will still be able to save new changes without I get a concurrency exception. why are the concurrent updates here not raising an update exception?

this is the Fluent API config,

 builder.Entity<PatientRegistry>()
        .Property(a => a.RowVersion).IsRowVersion()
        .IsConcurrencyToken()
        .ValueGeneratedOnAddOrUpdate();

I have the update log here

UPDATE

For future reference,

Based on the docs here, we can test for concurrency conflict by just manipulating one of the values before calling save.

i.e.

context.Database.ExecuteSqlCommand("UPDATE dbo.PatientsRegistry SET AliasName = 'Jane' WHERE PatientFileId = 2222");
await context.SaveChangesAsync();

this will raise up an exception.


Solution

  • If the read from second user is after the write from first user then the second user's RowVersion will reflect the value at the time (i.e. after the first update). Thus - no concurrency issue.

    You will only get an issue if they both read (and thus get the same RowVersion) and then both try and write.