In the
first part of the article, we studied how to find a parallel access conflict and possible ways to solve them.
The second part of the article is devoted to solving this conflict, when using LINQ to SQL.
The second part of the article describes how to resolve conflicts of parallel access in LINQ to SQL, and the reasons for the appearance of a ChangeConflictException when trying to update records, methods of solving.
Conflict detection
As mentioned in Part 1 of the article, we can use a special version field in which the version or date of the change is stored, or indicate previous field values in the WHERE clause to ensure that they have not been changed.
To use the version field, it is necessary in the entity class (* .designer.cs) described by the table, mark this field with the IsVersion property in the Column attribute. In this case, only this field will be involved to determine if a concurrent access conflict occurs.
')
If none of the fields is marked as IsVersion, then LINQ to SQL allows us to control which fields will be involved in conflict detection. For this, in the entity class, you must set the corresponding value of the UpdateCheck property of the Column attribute. It can be 3 possible values:
- Never - indicates that this field will not participate in the detection of a conflict.
- Always (default) - indicates that this field will always participate in the detection of a conflict, regardless of whether its value has been updated since it was first loaded into the cache of the DataContext object.
- WhenChanged - this field will participate if you update it and try to save the new value.
And so, we proceed to the practical part. First, create a test table:
CREATE TABLE Customers ([CustomerID][nvarchar](5) PRIMARY KEY , [CompanyName][nvarchar](40), [ContactName][nvarchar](30), [ContactTitle][nvarchar](30))
* This source code was highlighted with Source Code Highlighter .
Then go to our project in Visual Studio and add our table to the project (Add New Item - Linq to Sql Classes).


Now open the file MyTestDB.designer.cs and proceed to the description of the class
[global::System.Data.Linq.Mapping.TableAttribute(Name= "dbo.Customers" )]
public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
* This source code was highlighted with Source Code Highlighter .
It describes all the fields and their properties.
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CustomerID" , DbType= "NVarChar(5) NOT NULL" , CanBeNull= false , IsPrimaryKey= true )]
public string CustomerID …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CompanyName" , DbType= "NVarChar(40)" )]
public string CompanyName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactName" , DbType= "NVarChar(30)" )]
public string ContactName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactTitle" , DbType= "NVarChar(30)" )]
public string ContactTitle…
* This source code was highlighted with Source Code Highlighter .
As I already wrote, the UpdateCheck property, although not specified, has a default value of Always. This means that all fields are validated. Let's see this. Write the following code:
MyTestDBDataContext db = new MyTestDBDataContext();
db.Log = Console .Out;
Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP" ).SingleOrDefault();
cust.ContactName = "Neo Anderson" ;
db.SubmitChanges();
* This source code was highlighted with Source Code Highlighter .
After it is launched, the following will be displayed in the console window:
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
UPDATE [dbo].[Customers]
SET [ContactName] = @p4
WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND ([ContactTitle] = @p3)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input NVarChar (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant]
-- @p2: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]
-- @p3: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [Sales Manager]
-- @p4: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
Those. here we see how LINQ to SQL converted our query and executed it. Update clearly shows that during the update, all the fields involved in the detection of conflicts. If we do not need this, then we can manually set the values of the UpdateCheck properties to control. For example, we can establish that CompanyName will always be involved in the detection of a conflict, ContactName only when it changes, and ContactTitle will never be. Then it will look like this:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CustomerID" , DbType= "NVarChar(5) NOT NULL" , CanBeNull= false , IsPrimaryKey= true )]
public string CustomerID …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_CompanyName" , DbType= "NVarChar(40)" , UpdateCheck = UpdateCheck.WhenChanged)]
public string CompanyName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactName" , DbType= "NVarChar(30) " , UpdateCheck = UpdateCheck.Always)]
public string ContactName …
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_ContactTitle" , DbType= "NVarChar(30)" , UpdateCheck = UpdateCheck.Never)]
public string ContactTitle…
* This source code was highlighted with Source Code Highlighter .
And if we rerun our code, the Linq to Sql request will be different:
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
UPDATE [dbo].[Customers]
SET [ContactName] = @p3
WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input NVarChar (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant]
-- @p2: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]
-- @p3: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
As you can see, there is a difference! The CompanyName field did not participate, and ContactName participated because it was changed.
And so, conflict conflict detection occurs when you call SubmitChanges (), and if a conflict occurs, then you can manage the conflict resolution process. You can indicate whether further updates should be interrupted during the first conflict, or you can try to make all the changes accumulating conflicts. For this, you need to pass ConflictMode when you call SubmitChanges. If you pass ConflictMode.FailOnFirstConflict, then the process will be interrupted at the first conflict, another value is ConflictMode.ContinueOnConflict. By default, if you do not specify it, then ConflictMode.FailOnFirstConflict is used.
Regardless of whether you specified a transaction or not (in which case it will be created for all attempts to change the database), when an exception is thrown, the transaction will be canceled. This means that if a part of the changes were successfully updated, they will be canceled in case of an error.
ChangeConflictException Exception
Regardless of whether ConflictMode is set to FailOnFirstException or ContinueOnConflict, a ChangeConflictException exception will still be thrown.
By catching this exception, you can detect the occurrence of a conflict.
Conflict resolution
As soon as you find a conflict, catch the ChangeConflictException exception, your next step will most likely be to resolve it. In LINQ to SQL there are two methods ResolveAll and two methods Resolve.
Refreshmode
When we want to resolve a conflict using the built-in LINQ to SQL functionality by calling the ResolveAll or Resolve method, we control the way the conflict is resolved by specifying the RefreshMode mode. There are three valid values:
- KeepChanges tells the ResolveAll or Resolve method to update the property values of the entity class from the database, but if the user changes the property, its value is saved (i.e. your changes will not be lost)
- KeepCurrentValues indicates that your user’s changes should be used, rejecting all changes made in the database (values in the database will be overwritten by yours, which were read during the initial load)
- OverwriteCurrentValues, indicates that you need to discard your changes and load values from the database
Conflict Resolution Approaches
There are three approaches to conflict resolution: the simplest, easy and manual. The simplest is to simply call the ResolveAll method on the DataContext.ChangeConflicts collection with a RefreshMode pass and an optional Boolean value indicating whether to automatically enable remote records
(in this case, LINQ to SQL represents that the records to be deleted were successfully deleted because who has already deleted them before us)An easy approach is to enumerate all objectChangeConflict from the DataContext.ChangeConflicts collection with a call to the Resolve method on each of them.
In the manual mode, you enumerate the ChangeConflicts elements of the DataContext object, and then enumerate all the MemberConflicts elements of the ObjectChangeConflicts object, calling a Resolve on each MemberChangeConflict object from this collection.
DataContext.ChangeConflicts.ResolveAll ()
Conflict resolution is not difficult. You simply catch the ChangeConflictException exception and call the ResolveAll () method on the DataContext.ChangeConflicts collection. All that is required of you is to decide which RefreshMode mode to use, and whether you want to automatically resolve conflicts of deleted records. The use of this approach will cause the same resolution of all conflicts.
cust.ContactName = "Neo Anderson" ;
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
{
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console .WriteLine( " , ." );
}
}
}
* This source code was highlighted with Source Code Highlighter .
In this example, we first call ResolveAll, and then call the SubmitChanges method again. If it causes an error, then we roll back.
ObjectChangeConflict.Resolve ()
If the resolution of conflicts with the same RefreshMode or autoResolveDeletes does not work for you, you can choose an approach with enumerating all the conflicts from the DataContext.ChangeConflicts collection and process them individually.
cust.ContactName = "Neo Anderson" ;
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
{
Console .WriteLine( " {0}" , ((Customer)conflict.Object).CustomerID);
conflict.Resolve(RefreshMode.KeepChanges);
Console .WriteLine( " . {0}" , System.Environment.NewLine);
}
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console .WriteLine( " , ." );
}
}
}
* This source code was highlighted with Source Code Highlighter .
Here, similar to the ResolveAll method, you enumerate the ChangeConflicts collection and call Resolve on each ObjectChangeConflict object.
MemberChangeConflict.Resolve ()
For an example of manual conflict resolution, suppose that there is a requirement that if there is a conflict with the ContactName columns in the database, the code should leave the base value as it is, but any other columns in the record can be updated.
To implement this, we use the same basic approach, but instead of calling Resolve on an ObjectChangeConflict object, we enumerate the members of the MemberConflicts collection of each object. Then for each MemberConflict object from this collection, if the property of the entity object that caused the conflict is ContactName, then we will leave the value in the database, passing RefreshMode.OverwriteCurrentValues, to the Resolve method. If the conflicting property is not ContactName, then we will update it on ours by passing the RefreshMode.KeepChanges value to the Resolve method.
cust.ContactName = "Neo Anderson" ;
cust.CompanyName = "Lonesome & Pine Restaurant" ;
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
{
Console .WriteLine( " {0}" , ((Customer)conflict.Object).CustomerID);
foreach (MemberChangeConflict memberConflict in conflict.MemberConflicts)
{
if (memberConflict.Member.Name.Equals( "ContactName" ))
memberConflict.Resolve(RefreshMode.OverwriteCurrentValues);
else
memberConflict.Resolve(RefreshMode.KeepChanges);
}
Console .WriteLine( " . {0}" , System.Environment.NewLine);
}
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console .WriteLine( " , ." );
}
}
}
* This source code was highlighted with Source Code Highlighter .
A valid code that handles the resolution of a conflict manually is not so bad. But of course, all these efforts are justified only for specialized conflict resolution.
UPDATE: using the Version column
For the Version column, you can use the timestamp or int. For example, let's add a column to our table that will track the version of a record.
alter table Customers ADD [Version][rowversion] NOT NULL
* This source code was highlighted with Source Code Highlighter .
rowversion is an automatically updated field when a record is updated; the timestamp is used internally. Those. no matter how we update, via LINQ to SQL, or whatever, the field value will automatically increase. Of course, you can use a different way to maintain the version number, for example, as a number and update it with a trigger. In this case, for example, I used the rowversion type
Now you will need to update the MyTestDB.dbml view so that this new field appears in our project. After the update, our field will look like this:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage= "_Version" , AutoSync=AutoSync.Always, DbType= "rowversion NOT NULL" , CanBeNull= false , IsDbGenerated= true , IsVersion= true , UpdateCheck=UpdateCheck.Never)]
public System.Data.Linq.Binary Version
* This source code was highlighted with Source Code Highlighter .
When using IsVersion = true, you can forget about UpdateCheck, it will not be applied !!! The version field will always be used to search for conflicts. Other properties: IsDbGenerated - indicates that this field is generated by the database, cannot be changed, CanBeNull does not allow an empty value. The IsVersion field assumes immediate synchronization after inserting or updating a record.
And now let's check how LINQ to SQL queries have changed, after implementing the version field, on the following code:
MyTestDBDataContext db = new MyTestDBDataContext();
db.Log = Console .Out;
Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP" ).SingleOrDefault();
string name = cust.ContactName;
cust.ContactName = "Neo Anderson" ;
Console .WriteLine( " - {0} {1}" , BitConverter.ToString(cust.Version.ToArray()), System.Environment.NewLine);
db.SubmitChanges();
Console .WriteLine( " - {0} {1}" , BitConverter.ToString(cust.Version.ToArray()), System.Environment.NewLine);
cust.ContactName = name;
db.SubmitChanges();
Console .ReadKey();
* This source code was highlighted with Source Code Highlighter .
Here I added a couple of lines so that you can run the example several times (in the previous examples above, I did not add them to the example, although I used it myself). This will allow you to perform an example several times and always see the result. Because, as if the data did not change after caching, they will not be sent to the database for updating!
And so, I also added a version number mapping to make sure the version is actually updated. Result below:
SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Version]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
- 00-00-00-00-00-00-07-E9
UPDATE [dbo].[Customers]
SET [ContactName] = @p2
WHERE ([CustomerID] = @p0) AND ([Version] = @p1)
SELECT [t1].[Version]
FROM [dbo].[Customers] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[CustomerID] = @p3)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]
-- @p3: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
- 00-00-00-00-00-00-07-EA
UPDATE [dbo].[Customers]
SET [ContactName] = @p2
WHERE ([CustomerID] = @p0) AND ([Version] = @p1)
SELECT [t1].[Version]
FROM [dbo].[Customers] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[CustomerID] = @p3)
-- @p0: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]
-- @p3: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [LONEP]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.4926
As you can see, only the version field is used to find a conflict. In addition, after the update, it is immediately synchronized with the database, getting the current value.
Intercepting exceptions and working with them will be exactly the same as the examples above.
The last UPDATE and SELECT appeared due to the fact that we restore the original value.Pessimistic approach
With a pessimistic approach to concurrency, there are no conflicts that need to be resolved, because the database is locked by your transaction, so no one else can modify it behind you.
using (System.Transactions.TransactionScope tranaction = new System.Transactions.TransactionScope())
{
Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP" ).SingleOrDefault();
cust.ContactName = "Neo Anderson" ;
cust.CompanyName = "Lonesome & Pine Restaurant" ;
db.SubmitChanges();
tranaction.Complete();
}
* This source code was highlighted with Source Code Highlighter .
With this approach, you should always evaluate how much work you do inside the TransactionScope context, because the database will be blocked for all this time.
LINQ to SQL: Learn more about Joseph Rattz Jr. "LINQ Integrated Query Language in C # 2008 for Professionals"
UPD : added an example for the version field