EF Core Handling Concurrency Conflicts

Karen Payne - Jan 21 '23 - - Dev Community

The average developer tends not to think about concurrency violations until it happens which can compromise critical data. Dependent on their proficiency with C# and EF Core if at a novice level to intermediate level figuring out how to deal with concurrency violations may very well halt coding for hours or days until they master dealing with concurrency violations.

Although Microsoft provides a simple example, this article and code samples will assist in mastering dealing with concurrency violations. Please note that it is impossible to cover all scenarios but using provided code will assist in other cases not covered.

One might say, I only write applications for myself. Well this is said by many who later go on to share their work and end up with concurrency violations. For those developer consider cloning the repository for a possible future where this might happen.

From Microsoft

Database concurrency refers to situations in which multiple processes or users access or change the same data in a database at the same time. Concurrency control refers to specific mechanisms used to ensure data consistency in presence of concurrent changes.

EF Core implements optimistic concurrency control, meaning that it will let multiple processes or users make changes independently without the overhead of synchronization or locking. In the ideal situation, these changes will not interfere with each other and therefore will be able to succeed. In the worst case scenario, two or more processes will attempt to make conflicting changes, and only one of them should succeed.

Source code

Clone the following GitHub repository

Tips

  • Run the code
  • Run the code with the debugger
  • Go back and study all code presented

Concurrency check setup

To setup for concurrency checks, in the model below we use ConcurrencyCheck on properties in a model we want to know about concurrency violations.

public class Taxpayer
{
    public int Id { get; set; }
    [ConcurrencyCheck]
    public string FirstName { get; set; }

    [ConcurrencyCheck]
    public string LastName { get; set; }

    [JsonIgnore]
    public string FullName => $"{FirstName} {LastName}";
    [ConcurrencyCheck]
    public string SSN { get; set; }

    [JsonIgnore]
    public string SocialSecurityNumber =>
        SSN.Insert(5, "-").Insert(3, "-");

    [ConcurrencyCheck]
    public string Pin { get; set; }

    [ConcurrencyCheck]
    public DateTime? StartDate { get; set; }

    [ConcurrencyCheck]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    public override string ToString() => $"{FirstName} {LastName}";
}
Enter fullscreen mode Exit fullscreen mode

Or Fluent API (in the DbContext)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new Configurations.TaxpayerConfiguration());

    modelBuilder.Entity<Taxpayer>()
        .Property(t => t.FirstName)
        .IsConcurrencyToken();

    modelBuilder.Entity<Taxpayer>()
        .Property(t => t.LastName)
        .IsConcurrencyToken();

    modelBuilder.Entity<Taxpayer>()
        .Property(t => t.SSN)
        .IsConcurrencyToken();

    modelBuilder.Entity<Taxpayer>()
        .Property(t => t.Pin)
        .IsConcurrencyToken();

    modelBuilder.Entity<Taxpayer>()
        .Property(t => t.StartDate)
        .IsConcurrencyToken();

    modelBuilder.Entity<Taxpayer>()
        .Property(t => t.CategoryId)
        .IsConcurrencyToken();

    OnModelCreatingPartial(modelBuilder);
}
Enter fullscreen mode Exit fullscreen mode

Setup for testing

Using the console project TaxpayerMocking which reads the DbContext in the class project TaxpayerLibraryEntityVersion to create a new database with random data using NuGet package Bogus. This needs to be done as each time the main project (in this case) TaxpayersConcurrencyCheck will modify data so that a DbUpdateConcurrencyException is throw.

There are three scenarios, when a concurrency violation is thrown

  • Use the local values of the application, write to a log file.
  • Use the values in the database, write to a log file
  • Take no action, inform the caller the save changes failed and write to a log file

Favor local changes

First obtain a single Taxpayer, once read, perform an update to the database with values which may have come from another user just saving the record we are working on which in turn when this app attempts to execure SaveChanges, EF Core will throw a DbUpdateConcurrencyException. Next we make changes to properties of the Taxpayer just read from the database followed by attempting to save changes to the database.

Code to set the stage for saving to the database

private static async Task UpdateWithDatabaseValues()
{
    int id = 1;
    var taxpayer = await DataOperations.GetTaxpayerByIdentity(id);

    await DataOperations.Context.Database.ExecuteSqlRawAsync(
        "UPDATE dbo.Taxpayer SET FirstName = 'Dummy first name', LastName = 'Dummy last name' " +
        $"WHERE Id = {id}");

    var date = new RandomDateTime();
    taxpayer.FirstName = "Karen";
    taxpayer.LastName = "Payne";
    taxpayer.Pin = "4873";
    taxpayer.StartDate = date.Next();
    var (success, exception) = await DataOperations.UpdateWithCurrentDatabaseValues(taxpayer);
}
Enter fullscreen mode Exit fullscreen mode

Code to attempt to save change with a try/catch

  1. Setup for retry on failure
  2. In the try/catch, determine if a EntityEntry is a Taxpayer
  3. First get values that are from the caller using PropertyValues proposedValues = entry.CurrentValues;
  4. Next get current values in the database (as per above when we UPDATE dbo.Taxpayer...)
  5. Stored values in a StringBuilder.
  6. Log above into the log file
  7. entry.OriginalValues.SetValues(databaseValues!) sets the local values to when looping back to await Context.SaveChangesAsync();
public static async Task<(bool, NotSupportedException)> UpdateWithCurrentLocalValues(Taxpayer taxpayer)
{
    var saved = false;
    int iteration = 1;
    while (!saved)
    {
        try
        {
            AnsiConsole.MarkupLine($"[cyan]Invoking save changes[/] [white on blue]{iteration}[/]");
            iteration++;
            await Context.SaveChangesAsync();
            saved = true;
            return (true, null)!;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            AnsiConsole.MarkupLine("[red]In catch[/]");
            const string header = "Name                     Local value               Database value";

            foreach (var entry in ex.Entries)
            {
                if (entry.Entity is Taxpayer)
                {
                    PropertyValues proposedValues = entry.CurrentValues;
                    PropertyValues? databaseValues = await entry.GetDatabaseValuesAsync();

                    StringBuilder builder = new();

                    builder.AppendLine("");
                    builder.AppendLine($"{nameof(DataOperations)}.{nameof(UpdateWithCurrentLocalValues)}");
                    builder.AppendLine("DbUpdateConcurrencyException");
                    builder.AppendLine(header);

                    foreach (IProperty property in proposedValues.Properties)
                    {

                        var name = property.Name;
                        var value = proposedValues[property];
                        var dbValue = databaseValues?[property];
                        if (property.IsKey())
                        {
                            builder.AppendLine($"Key {value,22}");
                        }

                        if (!value!.Equals(dbValue))
                        {
                            builder.AppendLine($"{name,-24} {value,-25} {databaseValues![property]}");
                        }
                        var proposedValue = proposedValues[property];

                        proposedValues[property] = proposedValue;
                    }

                    // let's record values to file
                    AnsiConsole.MarkupLine("[cyan]Writing to log[/]");
                    SeriControl.Instance.Logger.Information(builder.ToString());

                    AnsiConsole.MarkupLine("[cyan]Setting to local values[/]");
                    entry.OriginalValues.SetValues(databaseValues!);
                }
                else 
                {

                    return (false, new NotSupportedException(
                        "Don't know how to handle concurrency conflicts for " + 
                        entry.Metadata.Name));

                }
            }
        }
    }

    return (false, null)!;
}
Enter fullscreen mode Exit fullscreen mode

Favor database values

The same path is followed as favor local values, the difference is var databaseValue = databaseValues![property]; vs var proposedValue = proposedValues[property];

No changes

This is simple, any violation performs no actions other than returning to the caller the save changes failed and write to a log file.

public static async Task<(bool, DbUpdateConcurrencyException ex)> Update(Taxpayer taxpayer)
{
    try
    {
        await Context.SaveChangesAsync();
        return (true, null)!;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        StringBuilder builder = new();
        builder.AppendLine("There were conflicts, aborting saving data");
        builder.AppendLine($"In {nameof(DataOperations)}.{nameof(Update)}");
        builder.AppendLine("This is correct for this method");
        SeriControl.Instance.Logger.Information(builder.ToString());
        return (false, ex);
    }

}
Enter fullscreen mode Exit fullscreen mode

Peeking at changes

This method can be helpful to examine changes, in this case of a Taxpayer prior to executing SaveChanges for validation or for learning purposes.

public static Task<List<EntityChangeItem>> GetChanges( Taxpayer taxpayer)
{

    List<EntityChangeItem> changes = new();

    var entry = Context.Entry(taxpayer);

    foreach (IProperty item in entry.CurrentValues.Properties)
    {
        PropertyEntry propEntry = entry.Property(item.Name);

        if (!propEntry.IsModified) continue;

        changes.Add(new EntityChangeItem()
        {
            PropertyName = item.Name,
            OriginalValue = propEntry.OriginalValue,
            CurrentValue = propEntry.CurrentValue
        });
    }

    return Task.FromResult(changes);

}
Enter fullscreen mode Exit fullscreen mode

Log file caveats

Only the favor local changes does a detailed log while the others don't but there is no reason a developer could not add this code. I left it this way as some developer may not carry to log the details when a violation occurs.

📌 Serilog NuGet package is used to log what goes on under the covers which will help to better understand how dealing with concurrency conflicts works and what information is available to a developer.

2022-09-18 09:12:41.227 -07:00 [INF] There were conflicts, aborting saving data
DataOperations.Update

2022-09-18 09:17:57.689 -07:00 [INF] There were conflicts, aborting saving data
In DataOperations.Update
Using values from the database

2022-09-18 09:18:33.260 -07:00 [INF] 
DataOperations.UpdateWithCurrentLocalValues
DbUpdateConcurrencyException
Name                     Local value               Database value
Key                      1
FirstName                Karen                     Dummy first name
LastName                 Payne                     Dummy last name
Pin                      4873                      8053
SSN                      111223333                 866801328
StartDate                8/19/2003 5:04:01 AM      8/18/2021 12:00:00 AM
Enter fullscreen mode Exit fullscreen mode

See also

Summary

What has been presented provides basic to intermediate level code samples for dealing with handling concurrency conflicts so that in your application that has the possibility of others working on the same record to understand how to deal with this possibility no matter how remote it maybe it is highly recommended to consider one of the three options presented.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .