Let us assume we have a person entity and it looks as below
Imports System.ComponentModel.DataAnnotations
Public Class Person
Public Property ID As Integer
<StringLength(50)>
Public Property FirstName As String
<Timestamp()>
Public Property RowVersion As Byte()
End Class
Using EF 4.1 and code first, a typical update would look as show below
Using entities = New EFConcurrencyTestContext
Try
Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
personEntity.FirstName = "CCC"
entities.SaveChanges()
Catch concurrencyException As DbUpdateConcurrencyException
Throw New Exception("The record u r trying to update has changed")
Catch ex As Exception
Throw New Exception("Some Exception")
End Try
End Using
Since our main focus is to test Concurrency, let put a break point at the line Highlighted, start debugging when the debugger stops at personEntity.FirstName = "CCC", in the database update the person firstname, then continue debugging, on the savechanges you would get a DbUpdateConcurrencyException. This is pretty straight forward and works as expected. Let’s take a different scenario, for e.g. in multi tier MVC application
1. In the service layer I would do get person, say the person FirstName is ‘A’, RowVersion is 1
2. I would then build a ViewModel, take only a few data elements from person entity I need for the viewModel, I don’t transfer the person entity across tiers, meaning my controller only know about my viewmodel and is unaware of my domain object which is person. In order to check for Concurrency I will create a property in my ViewModel called PersonRowVersion and store the rowversion in it.
3. As the data is being manipulated by my view, let us assume the record in the database was updated as well. Say FirstName in the Database is ‘B’ and RowVersion is 2
4. Now my View modified the person FirstName to ‘C’, I had stored my RowVersion either using a hidden field or in the Tempdata. I build the ViewModel, take the modified FirstName which is ‘C’ and restore the RowVersion from my hidden field which is 1
5. In the service layer, I get the person entity from the database, which will have the FirstName = ‘B” and RowVersion = “2”. I update the FirstName to “C” and RowVersion to “1” and do Context.Savechanges. The update happens but no concurrency exception is raised
6. In order to get the Concurrency Exception you will have to modify the OriginalValue of the property that is used for tracking concurrency, in my case it is RowVersion. Something like this
entities.Entry(personEntity).Property("RowVersion").OriginalValue = _rowVersion
I have created a few test cases (Windows client application) to mimic this process. The idea is to show what works and what does not.
Imports System.Data.Entity.Infrastructure
Public Class Form1
Private _fName As String
Private _rowVersion As Byte()
Private _person As Person
Private Sub CreateDB_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CreateDB.Click
Using entities = New EFConcurrencyTestContext
Dim personEntity As New Person With {.FirstName = "AAA"}
entities.People.Add(personEntity)
entities.SaveChanges()
End Using
End Sub
Private Sub Get1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Get1.Click
Using entities = New EFConcurrencyTestContext
Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
_fName = personEntity.FirstName
_rowVersion = personEntity.RowVersion
_person = personEntity
End Using
End Sub
Private Sub TestConcurrencyWrongWayButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TestConcurrencyWrongWayButton.Click
Using entities = New EFConcurrencyTestContext
Try
Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
personEntity.FirstName = "CCC"
personEntity.RowVersion = _rowVersion
entities.SaveChanges()
Catch concurrencyException As DbUpdateConcurrencyException
Throw New Exception("The record u r trying to update has changed")
Catch ex As Exception
Throw New Exception("Some Exception")
End Try
End Using
End Sub
Private Sub TestConcurrencyRightWayButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TestConcurrencyRightWayButton.Click
Using entities = New EFConcurrencyTestContext
Try
Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
personEntity.FirstName = "CCC"
entities.Entry(personEntity).Property("RowVersion").OriginalValue = _rowVersion
entities.SaveChanges()
Catch concurrencyException As DbUpdateConcurrencyException
Throw New Exception("The record u r trying to update has changed")
Catch ex As Exception
Throw New Exception("Some Exception")
End Try
End Using
End Sub
Private Sub TestConcurrencyWithAttachButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TestConcurrencyWithAttachButton.Click
Using entities = New EFConcurrencyTestContext
Try
_person.FirstName = "xxxx"
entities.Entry(_person).State = EntityState.Modified
entities.SaveChanges()
Catch concurrencyException As DbUpdateConcurrencyException
Throw New Exception("The record u r trying to update has changed")
Catch ex As Exception
Throw New Exception("Some Exception")
End Try
End Using
End Sub
Private Sub ModifiedRepositoryWithOriginalRowVersionButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ModifiedRepositoryWithOriginalRowVersionButton.Click
Dim _repository As New TestRepository(New EFConcurrencyTestContext)
Try
Dim personEntity = _repository.GetQuery(Of Person).Where(Function(p) p.ID = 1).FirstOrDefault
personEntity.FirstName = "CCC"
_repository.SetRowVersion(Of Person)(personEntity, _rowVersion)
_repository.Save()
Catch concurrencyException As DbUpdateConcurrencyException
Throw New Exception("The record u r trying to update has changed")
Catch ex As Exception
Throw New Exception("Some Exception")
End Try
End Sub
End Class
Test Case 1
1. Click Get 1 – Person data is got from the database and stored in memory
a. Person.FirstName = A
b. Person.RowVersion = 1
2. Update the Person Table FirstName = B, the row version will be 2
3. Click TestConcurrencyWorngWay, Update happens no concurrency exception
Test Case 2
Repeat 1 and 2 as in Test Case1, Click TestConcurrencyRightWay. No Update, concurrency exception is raised
Test Case 3
You can also get the concurrency exception to work by doing attach, but you got to store the person entity in memory to attach it to the context. In multi tier MVC application if you are using View model to carry data to the controller, then this becomes a problem as you need to store the domain object in memory. But to test it repeat 1 and 2 as in test case 1 and then click TestConcurrencyWith Attach, no update happens concurrency exception occurs
Test Case 4
Sometime you may be using repository pattern in your application, to test this repeat 1 and 2 as in test case 1, and then click ModifiedRepositoryWithOriginalRowVersion. Below is a sample repository
Imports System.Data.Entity
Imports System.Data.Entity.Design.PluralizationServices
Imports System.Globalization
Imports System.Data.Entity.Infrastructure
Public Class TestRepository
Private _context As DbContext
Private ReadOnly _pluralizer As PluralizationService = PluralizationService.CreateService(CultureInfo.GetCultureInfo("en"))
Public Sub New(ByVal context As DbContext)
If context Is Nothing Then
Throw New ArgumentNullException("context")
End If
Me._context = context
End Sub
Private Function GetEntityName(Of TEntity As Class)() As String
Return String.Format("{0}.{1}", DirectCast(_context, IObjectContextAdapter).ObjectContext.DefaultContainerName, _pluralizer.Pluralize(GetType(TEntity).Name))
End Function
Public Sub Save()
_context.SaveChanges()
End Sub
Public Sub SetRowVersion(Of TEntity As Class)(ByVal entity As TEntity, ByVal originalRowVersion As Byte())
_context.Entry(entity).Property("RowVersion").OriginalValue = originalRowVersion
End Sub
Public Function GetQuery(Of TEntity As Class)() As IQueryable(Of TEntity)
Dim entityName = GetEntityName(Of TEntity)()
Return DirectCast(_context, IObjectContextAdapter).ObjectContext.CreateQuery(Of TEntity)(entityName)
End Function
End Class