Most applications have to deal with concurrency. Concurrency is a circumstance where two users modify the same entity at the same time. There are two types of concurrency handling: optimistic and pessimistic. There is no concurrency where the last user always wins. When this happens, there is a silent data loss, where the first user's changes are overwritten without notice, so it is not frequently used. In the case of pessimistic concurrency, only one user can edit a record at a time and the second user gets an error, stating that they cannot make any changes at that time. Although this approach is safe, it does not scale well and results in poor user experience. As a result, most applications use optimistic concurrency, allowing multiple users to make changes, but checking for a concurrency situation at the time changes are being saved. At that time if two users changed the same row of data, applications issue an error to the second user, letting them know that they need to redo the changes. Some developers at times go an extra mile and assist users in redoing their changes. Entity Framework comes with a built-in optimistic concurrency API. A developer has to pick a column that will play the role of the row version. The row version is incremented every time the row of data is updated. Any time an update query is issued against a row with concurrency columns, the current row version is put into the where
clause; thus if data has changed since it was first retrieved, no rows are updated as the result of such SQL statement. Entity Framework checks the number of rows updated, and if this number is not 1
, a concurrency exception is thrown. In the case of the SQL Server RowVersion
, also known as TimeStamp
, a column is used for concurrency. SQL Server automatically increments this column for all updates to each row. The matching type for the TimeStamp
column in SQL Server is byte array
in .NET. Let's start by updating our Person
object to support concurrency. We are going to omit some properties for brevity, as shown in the following code snippet:
public class Person { public int PersonId { get; set; } public byte[] RowVersion { get; set; } }
We added a new property called RowVersion
using the Byte
array as the type. Here is how this change looks in VB.NET:
Public Class Person Property PersonId() As Integer Property RowVersion() As Byte() End Class
We also need to configure this property using our EntityTypeConfiguration
class to let Entity Framework know that we added a concurrency property, as shown in the following code snippet:
public class PersonMap : EntityTypeConfiguration<Person> { public PersonMap() { Property(p => p.RowVersion) .IsFixedLength() .HasMaxLength(8) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed) .IsRowVersion(); } }
We omitted some properties again, but configured RowVersion
to be our concurrency column. We flagged it as such by calling the IsRowVersion
method, as well as configuring the size for SQL Server and flagging it for Entity Framework as database generated. Technically, we only need to call the IsRowVersion
method, but this code makes it clear as to how the property is configured. We can and should remove other method calls, as they are not needed. Here is how VB.NET code looks:
Public Class PersonMap Inherits EntityTypeConfiguration(Of Person) Public Sub New() Me.Property(Function(p) p.RowVersion) _ .IsFixedLength() _ .HasMaxLength(8) _ .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed) _ .IsRowVersion() End Sub End Class
Now we are ready to write some code to ensure our concurrency configuration works. It is hard to simulate two users in a single routine, so we will play some tricks, using the knowledge we gained previously, as shown in the following code:
private static void ConcurrencyExample() { var person = new Person { BirthDate = new DateTime(1970, 1, 2), FirstName = "Aaron", HeightInFeet = 6M, IsActive = true, LastName = "Smith" }; int personId; using (var context = new Context()) { context.People.Add(person); context.SaveChanges(); personId = person.PersonId; } //simulate second user using (var context = new Context()) { context.People.Find(personId).IsActive = false; context.SaveChanges(); } //back to first user try { using (var context = new Context()) { context.Entry(person).State = EntityState.Unchanged; person.IsActive = false; context.SaveChanges(); } Console.WriteLine("Concurrency error should occur!"); } catch (DbUpdateConcurrencyException) { Console.WriteLine("Expected concurrency error"); } Console.ReadKey(); }
This method is a bit lengthy, so let's walk through it. In the first few lines, we created a new person instance and added it to the database by adding it to the People
collection, and then calling SaveChanges
on our context. We then pretend to be a second user, updating the same row by calling the Find
method, changing one property, and then issuing the SaveChanges
call. This action will increment the row version inside the database. Next, we are pretended to be the first user, using the original person instance that still has the original row version value. We set the state to unmodified, thus attaching it to the context. Then, we changed a single property and saved the changes again. This time we get a specific concurrency exception of the DbUpdateConcurrencyException
type. This is how the code looks in VB.NET:
Private Sub ConcurrencyExample() Dim person = New Person() With { .BirthDate = New DateTime(1970, 1, 2), .FirstName = "Aaron", .HeightInFeet = 6D, .IsActive = True, .LastName = "Smith" } Dim personId As Integer Using context = New Context() context.People.Add(person) context.SaveChanges() personId = person.PersonId End Using 'simulate second user Using context = New Context() context.People.Find(personId).IsActive = False context.SaveChanges() End Using 'back to first user Try Using context = New Context() context.Entry(person).State = EntityState.Unchanged person.IsActive = False context.SaveChanges() End Using Console.WriteLine("Concurrency error should occur!") Catch exception As DbUpdateConcurrencyException Console.WriteLine("Expected concurrency error") End Try Console.ReadKey() End Sub
This exception handling code is something we always need to write when implementing concurrency. We need to show the user a nice descriptive message. At that point, they will need to refresh their data with the current database values and then redo the changes. If we, as developers, want to assist users in this task, we can use Entity Framework's DbEntityEntry
class to get the current database values.
3.137.183.210