Handling concurrency

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.183.210