Concurrency control in EF Core
e situation when another user has performed an operation that conflicts with the current operation is known as concurrency conflict.
Database providers are responsible for implementing the comparison of concurrency token values.
On relational databases EF Core includes a check for the value of the concurrency token in the
WHERE
clause of any UPDATE
or DELETE
statements. After executing the statements, EF Core reads the number of rows that were affected. If no rows are affected, a concurrency conflict is detected, and EF Core throws DbUpdateConcurrencyException
.
There are two ways to define concurrency token:
1. by applying [ConcurrencyCheck] attribute like
public class Person
{
public int PersonId { get; set; }
[ ]
public string LastName { get; set; }
public string FirstName { get; set; }
}
2. by Adding timestamp property with [TimeStamp] attribute inside entity:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[ ]
public byte[] Timestamp { get; set; }
}
In Database query this property will be check like:
UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2; //here LastName property is configure as
//concurrency token.
There are three sets of values available to help resolve a concurrency conflict:
- Current values are the values that the application was attempting to write to the database.
- Original values are the values that were originally retrieved from the database, before any edits were made.
- Database values are the values currently stored in the database.
The general approach to handle a concurrency conflicts is:
- Catch
DbUpdateConcurrencyException
duringSaveChanges
. - Use
DbUpdateConcurrencyException.Entries
to prepare a new set of changes for the affected entities. - Refresh the original values of the concurrency token to reflect the current values in the database.
- Retry the process until no conflicts occur.
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);
}
}
}
}
}
No comments:
Post a Comment