0

I am just starting to learn about concurrent processing in EF Core to make consistent applications and I have some questions, Microsoft documentation says the following:

using var context = new PersonContext();

// Fetch a person from database and change phone number
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";

// Change the person's name in the database to simulate a concurrency conflict
context.Database.ExecuteSqlRaw(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

var saved = false;

while (!saved)
{
    try
    {
        // Attempt to save changes to the database
        context.SaveChanges();
        saved = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // TODO: decide which value should be written to database
                    // proposedValues[property] = <value to be saved>;
                }

                // Refresh original values to bypass next concurrency check
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException(
                    "Don't know how to handle concurrency conflicts for "
                    + entry.Metadata.Name);
            }
        }
    }
}

I have this code of the WeatherService:

public WeatherForecast UpdateMeasurementsForToday()
{
     var saved = false;
     var date = DateTime.Today;
     var specified = DateTime.SpecifyKind(date, DateTimeKind.Utc);

     while (!saved)
     {
         try
         {
             var entityFromDb = _source.GetWeatherByDate(specified);
             entityFromDb.TemperatureC = Random.Shared.Next(30);
             
             _source.UpdateEntity(entityFromDb);
             saved = true;
             return entityFromDb;
         }
         catch (DbUpdateConcurrencyException ex)
         {
             foreach (var entry in ex.Entries)
             {
                 if (entry.Entity is WeatherForecast)
                 {
                     var proposedValues = entry.CurrentValues;
                     var databaseValues = entry.GetDatabaseValues();

                     if (databaseValues is null)
                         throw new ArgumentException("Сущность была удалена");
                     entry.OriginalValues.SetValues(databaseValues);
                     entry.CurrentValues.SetValues(databaseValues);
                 }
                 else
                 {
                     throw new NotSupportedException(
                         "Don't know how to handle concurrency conflicts for "
                         + entry.Metadata.Name);
                 }
             }
         }
     }

     throw new ArgumentException();
}

Code from _source repository:

public void UpdateEntity(WeatherForecast newEntity)
{
    var entityFromDb = _ctx.Forecasts.First(c => c.Id.Equals(newEntity.Id));
    entityFromDb.TemperatureC = newEntity.TemperatureC;
    entityFromDb.SummaryUpdates++;
    entityFromDb.Version = Guid.NewGuid();
    _ctx.SaveChanges();
}

And I have a question, why when we get DbConcurrencyException we do some extra steps like in Miscrosoft documentation?

Can't we just go back and ask for NEW data (I tried this way and it really didn't work properly, the loop was endlessly throwing DbConcurrencyUpdateEx, I think it's due to the fact that EF Core caches data).

Another issue is when we may need a situation where we need our proposed values and NEW values from the database to resolve a conflict, because our proposed values are based on the old ones, logically it seems as if we just need to write

entry.OriginalValues.SetValues(databaseValues);
entry.CurrentValues.SetValues(databaseValues);

and kind of suggest new values.

But in the documentation we again go through the properties in detail.

Also, probably obvious for an insider, but I genuinely don't understand why my concurrency test is unstable:

public async void UpdateWeather_Updatingsimultaneously_SummaryShouldConsiderUpdateCounters()
{
     var rep1 = new EfWeatherRepository(_ctx1);
     var rep2 = new EfWeatherRepository(efDbInitializer.Ctx);
     var weatherService1 = new WeatherService(rep1, _fakeService.Object);
     var weatherService2 = new WeatherService(rep2, _fakeService.Object);
     var createdEntity = weatherService1.MeasureTodayWeather();
     var task1 = Task.Run(() => weatherService1.UpdateMeasurementsForToday());
     var task2 = Task.Run(() => weatherService2.UpdateMeasurementsForToday());
     await Task.WhenAll(task1, task2);
     var result = rep1.GetEntityById(createdEntity.Id);
     result.SummaryUpdates.Should().Be(2);
}

I'm intercepting the conflict, updating the data and loading the weather entity into the database with the NEW data, but sometimes the test fails because the number of updates is 1 instead of 2. Class definition

public class WeatherForecast : IUnique
{
    public int Id { get; set; }
    public DateTime Date { get; set; }
    public int SummaryUpdates { get; set; }
    public int TemperatureC { get; set; }
    [NotMapped]
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    [ConcurrencyCheck]
    public Guid Version { get; set; }
    public string? Summary { get; set; }
}

Sql table definition (I have only 1 table)

"forecasts" "public"    "Forecasts" "Id"    1       "NO"    "integer"
"forecasts" "public"    "Forecasts" "Date"  2       "NO"    "timestamp with time zone"
"forecasts" "public"    "Forecasts" "SummaryUpdates"    3       "NO"    "integer"
"forecasts" "public"    "Forecasts" "TemperatureC"  4       "NO"    "integer"
"forecasts" "public"    "Forecasts" "Version"   5       "NO"    "uuid"
"forecasts" "public"    "Forecasts" "Summary"   6       "YES"   "text"

I used the code from the MS documentation

18
  • Can you show your full SQL table definition, as well as the C# model Commented Jun 1, 2024 at 23:44
  • @Charlieface, yes public class WeatherForecast : IUnique { public int Id { get; set; } public DateTime Date { get; set; } public int SummaryUpdates { get; set; } public int TemperatureC { get; set; } [NotMapped] public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); [ConcurrencyCheck] public Guid Version { get; set; } public string? Summary { get; set; } } Commented Jun 2, 2024 at 21:23
  • @Charlieface, sry, tried to prettier this one, but it doesnt work in commets and i didnt found how to edit my question to add defition of entity Commented Jun 2, 2024 at 21:24
  • There is an edit button right there. Please show the table definition also, as well as any triggers. Commented Jun 3, 2024 at 0:31
  • 5
    "Can't we just go back and ask for NEW data" - of course we can. You might be forgetting the most vital part of your app though; your user. They just spent minutes (or longer) carefully editing that data; what will you do with their edits? What will you do with the other user's edits (the user that caused the concurrency issue)? Handling the case of two humans slowly editing the same db record isn't always "the last person to press save wins" - think about what you do when there are git merge conflicts - you take yours, take theirs or do a 3 way and blend the two edit sets. That needs a UI Commented Jun 3, 2024 at 8:59

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.