skip to Main Content

I have a given entity ‘Student’ with application based concurrency control defined on ‘LastModified’.

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public DateTimeOffset Created { get; set; }
    [ConcurrencyCheck]
    public DateTimeOffset LastModified { get; set; }
}

Now, I have update function written in repository layer as below:

public async Task<UpsertResponse> UpdateAsync(Entities.Student student)
{
    using var context = _dbContextFactory.CreateDbContext(readOnly: true); //Create reader DbContext instance
    var studentToBeModified = await context.Student.FirstOrDefaultAsync(x => x.Id == student.Id).ConfigureAwait(false);

    if (!studentToBeModified)
    {
        studentToBeModified.Name = student.Name;
        studentToBeModified.LastModified = DateTimeOffset.UtcNow;

        using var context = _dbContextFactory.CreateDbContext(readOnly: false); //Create writer DbContext instance
        context.Update(studentToBeModified);
        await context.SaveChangesAsync().ConfigureAwait(false); //Throws DbUpdateConcurrencyException
    }
}

Above problem can be fixed in below 2 ways with some Pros/Cons:

  1. Use one writer DbContext instance and use it for both read and write operation.
    Cons: Not a best way to distribute load on read and write db instance, as write instance is only 1 but read instances can scale to many
  2. Write SaveChangesAsync interceptor to take care to update LastModified value.
    Cons: Overhead to have an interceptor

What the better way to handle application based concurrency check using Entity Framework without impacting application performance?

Kindly share if another option is also available

2

Answers


  1. SaveChangesAsync() will suceeed if, before modifying properties of studentToBeModified, you use Attach() to start tracking it in the writer context.

    The body of your if would become:

    using var context = _dbContextFactory.CreateDbContext(readOnly: false); 
    context.Student.Attach(studentToBeModified);
    studentToBeModified.Name = student.Name;
    studentToBeModified.LastModified = DateTimeOffset.UtcNow;
    await context.SaveChangesAsync().ConfigureAwait(false); 
    
    Login or Signup to reply.
  2. To handle Optimistic Concurrency you can’t use a DateTime property generated by your application, because you will surely understand that in a multi-user environment there could be another user generating the same value for the same property, in the same instant.
    If you are connecting to SQL Server, a safer choice could be to add this property in each of your entity classes:

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    [ConcurrencyCheck]
    public byte[] RowVersion { get; set; }
    

    as Microsoft also recommends in its documentation. You should enclose your saving operations inside a "try…catch" block, and repeat it (ie. with a "while") if an Update Concurrency Exception occurs:

    Also, in your code, it’s convenient to use one DbContext only, for reading and writing, so studentToBeModified will be attached to the context when read, and you can modify and save it, in just a few steps. Entity Framework operations are an Unit of Work and must be concluded in a short time. So using one YourContext only, ContextFactory wouldn’t even be necessary. Anyway, I modified your code a bit:

    public async Task<UpsertResponse> UpdateAsync(Student student)
    {
        using (var context = _dbContextFactory.CreateDbContext())
        {
            while (true)
            {
                try
                {
                    var studentToBeModified = await context.Student.FirstOrDefaultAsync(x => x.Id == student.Id);
                    if (studentToBeModified != null)
                    {
                        studentToBeModified.Name = student.Name;
                        studentToBeModified.LastModified = DateTimeOffset.UtcNow;   //if you need to save update time       
                        await context.SaveChangesAsync();
                        break;
                    }
                }       
                catch (DbUpdateConcurrencyException ex)
                {
                    context.ChangeTracker.Clear();
                }       
            }
        
            ...your code continue there...
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search