The other frequent reason for errors, occurring when submitting changes, is concurrency conflicts. They aren't actually based on any server errors and are generated by the LinqConnect runtime to warn you that someone changed the data you are going to modify.
Let's describe such a situation in more details:
In this case, the intermediate change (the one performed by another user) may be lost or corrupt.
LinqConnect implements the optimistic concurrency approach: it is supposed that different users rarely change the same data, so the data are not locked when being retrieved from the database. But suppose the following occurs:
Unless some validations are performed at the last step, the intermediate change (the one performed by another user) will be lost.
To detect such situations, LinqConnect checks not only the primary key in the 'where' clause of update and delete commands, but the values of other columns as well. Thus, in case of a concurrency conflict the latter update will fail, meaning that no rows will be updated. From the server point of view, such an operation is correct: no actual error occurred, the only thing is that nothing was updated either. But for the application, the object state not being saved is clearly an error, so LinqConnect throws ChangeConflictException (the text is usually "Row not found or changed.").
If you do want to throw away all changes except the latest ones in such situations, or are sure that your application will be the only client of the database, you can disable all update checks to have no concurrency exceptions on submits and also some performance gain (because update commands become simpler and have less parameters).
Otherwise, you will probably face concurrency conflicts sometimes. Fortunately, LinqConnect provides the mechanism for processing such conflicts with little efforts.
Here is a code sample that should result in throwing ChangeConflictException:
C#csharp | Copy Code |
---|---|
public static void UpdateProductScale( DbConnection connection, int productId, int newScale) { try { connection.Open(); var command = connection.CreateCommand(); command.CommandText = "update \"Products\" set \"UnitScale\" = :scale where \"ProductID\" = :id"; var scaleParameter = command.CreateParameter(); scaleParameter.ParameterName = "scale"; scaleParameter.Value = newScale; command.Parameters.Add(scaleParameter); var idParameter = command.CreateParameter(); idParameter.ParameterName = "id"; idParameter.Value = productId; command.Parameters.Add(idParameter); command.ExecuteNonQuery(); } finally { connection.Close(); } } static void Main(string[] args) { CrmDemoDataContext context = new CrmDemoDataContext() { Log = Console.Out }; // Get a particular product from the database. Product product = context.Products .Where(p => p.ProductID == 7219) .SingleOrDefault(); // Ensure that the product was retrieved. if (product == null) return; // Here we simulate that some other client modifies // the row corresponding to this very product. UpdateProductScale(context.Connection, 7219, 2); // Here we modify this product and try to save the changes via LinqConnect. product.UnitName = "unit"; product.UnitScale = 1; try { context.SubmitChanges(ConflictMode.ContinueOnConflict); } // ... |
Visual Basic | Copy Code |
---|---|
Public Shared Sub UpdateProductScale( _ connection As DbConnection, _ productId As Integer, _ newScale As Integer) Try connection.Open() Dim command = connection.CreateCommand() command.CommandText = "update ""Products"" set ""UnitScale"" = :scale where ""ProductID"" = :id" Dim scaleParameter = command.CreateParameter() scaleParameter.ParameterName = "scale" scaleParameter.Value = newScale command.Parameters.Add(scaleParameter) Dim idParameter = command.CreateParameter() idParameter.ParameterName = "id" idParameter.Value = productId command.Parameters.Add(idParameter) command.ExecuteNonQuery() Finally connection.Close() End Try End Sub Private Shared Sub Main(args As String()) Dim context As New CrmDemoDataContext() With { _ .Log = Console.Out _ } ' Get a particular product from the database. Dim product As Product = context.Products.Where(Function(p) p.ProductID = 7219).SingleOrDefault() ' Ensure that the product was retrieved. If product Is Nothing Then Return End If ' Here we simulate that some other client modifies ' the row corresponding to this very product. UpdateProductScale(context.Connection, 7219, 2) ' Here we modify this product and try to save the changes via LinqConnect. product.UnitName = "unit" product.UnitScale = 1 Try context.SubmitChanges(ConflictMode.ContinueOnConflict) ' ... |
In the sample, a product entity is gotten from the database and modified. Before saving the changes, the row corresponding to this very product is changed by other client. We do this via 'plain' ADO.NET to avoid using the context with which we've got the product in the Main method (as in this case the context would be informed about all changes performed to the product, and no concurrency conflict would occur). Clearly, we could do the intermediate update via LinqConnect as well, however, another instances of CrmDemoContext and Product should be used in this case.
We call the SubmitChanges method with the ContinueOnConflict option: it specifies that the submit should not break after the first error occurs, but try to perform other updates instead to get the information about all conflicts (the submit, however, will fail anyway, and correct updates - if any - won't be done either). This is not really important for the above sample (since a single error occurs), we only use it to note that the resolve approach described below can be used for processing multiple update conflicts at once. As an alternative, the (default) ConflictMode.FailOnFirstConflict option may be used; in this case, no action will be performed after the first concurrency error.
So, when we try to submit the changes, a ChangeConflictException is thrown. If we catch the exception, we can then resolve the conflict(s) in the following way:
C#csharp | Copy Code |
---|---|
catch (ChangeConflictException ex) { foreach (ObjectChangeConflict objConflict in context.ChangeConflicts) { foreach (MemberChangeConflict memberConflict in objConflict.MemberConflicts) { memberConflict.Resolve(RefreshMode.KeepCurrentValues); } } context.SubmitChanges(ConflictMode.ContinueOnConflict); } |
Visual Basic | Copy Code |
---|---|
Catch ex As ChangeConflictException For Each objConflict As ObjectChangeConflict In context.ChangeConflicts For Each memberConflict As MemberChangeConflict In objConflict.MemberConflicts memberConflict.Resolve(RefreshMode.KeepCurrentValues) Next Next context.SubmitChanges(ConflictMode.ContinueOnConflict) End Try |
When a concurrency conflict occurs, the LinqConnect runtime queries the database for the current state of the entity that failed to be updated/deleted. Based on this query result, LinqConnect forms a collection of MemberChangeConflicts, each holding the supposed and actual values of the certain entity member. This collection is then enclosed into an ObjectChangeConflict, which holds the full information on the conflict.
Both MemberChangeConflict and ObjectChangeConflict classes have the Resolve method, which gives LinqConnect the instructions on what value (yours or the one currently stored in the database) should be persisted. These instructions are applied to a single member or the whole entity respectively. The possible approaches the Resolve member may carry out are specified in the ConflictMode enumeration:
After giving the instructions on all conflicts, we invoke SubmitChanges again to save the merged data. Though smaller, there is a probability that another update to the involved data occurs while you are resolving a concurrency issue. In this case, a ChangeConflictException is thrown again. To handle such situations, one can continue trying to submit changes in a loop, until such submit succeeds.
C#csharp | Copy Code |
---|---|
int maxSubmitAttempts = 3; bool successfullyUpdated = false; for (int currentAttempt = 0; !successfullyUpdated && currentAttempt < maxSubmitAttempts; currentAttempt++) { try { context.SubmitChanges(ConflictMode.ContinueOnConflict); successfullyUpdated = true; } catch (ChangeConflictException ex) { // Resolve the conflicts here. } } if (!successfullyUpdated) throw new ApplicationException( String.Format("Could not submit changes for {0} attempts.", maxSubmitAttempts)); |
Visual Basic | Copy Code |
---|---|
Dim maxSubmitAttempts As Integer = 3 Dim successfullyUpdated As Boolean = False Dim currentAttempt As Integer = 0 While Not successfullyUpdated AndAlso currentAttempt < maxSubmitAttempts Try context.SubmitChanges(ConflictMode.ContinueOnConflict) successfullyUpdated = True ' Resolve the conflicts here. Catch ex As ChangeConflictException End Try currentAttempt += 1 End While If Not successfullyUpdated Then Throw New ApplicationException( _ [String].Format("Could not submit changes for {0} attempts.", _ maxSubmitAttempts)) End If |
If you use this approach, it is recommended to use some maximal number of update attempts, to ensure that the loop does end eventually.
The above example covers a situation when both actions (yours and the one of some other user) are updating an entity, but what if either of them is a deletion? Let's consider such situations in more details:
Deleting an entity that has already been deleted. In this case, your deletion via the context object will affect no rows in the database, thus a change conflict exception will be thrown:
C#csharp | Copy Code |
---|---|
public static void DeleteProduct(DbConnection connection, int productId) { try { connection.Open(); var command = connection.CreateCommand(); command.CommandText = "delete from \"Products\" where \"ProductID\" = :id"; var idParameter = command.CreateParameter(); idParameter.ParameterName = "id"; idParameter.Value = productId; command.Parameters.Add(idParameter); command.ExecuteNonQuery(); } finally { connection.Close(); } } static void Main(string[] args) { CrmDemoDataContext context = new CrmDemoDataContext() { Log = Console.Out }; Product product = context.Products .Where(p => p.ProductID == 7219) .SingleOrDefault(); // Ensure that the product was retrieved. if (product == null) return; DeleteProduct(context.Connection, 7219); context.Products.DeleteOnSubmit(product); try { context.SubmitChanges(); } catch (ChangeConflictException ex) { foreach (ObjectChangeConflict changeConflict in context.ChangeConflicts) changeConflict.Resolve(RefreshMode.KeepChanges, true); context.SubmitChanges(); } } |
Visual Basic | Copy Code |
---|---|
Public Shared Sub DeleteProduct(connection As DbConnection, productId As Integer) Try connection.Open() Dim command = connection.CreateCommand() command.CommandText = "delete from ""Products"" where ""ProductID"" = :id" Dim idParameter = command.CreateParameter() idParameter.ParameterName = "id" idParameter.Value = productId command.Parameters.Add(idParameter) command.ExecuteNonQuery() Finally connection.Close() End Try End Sub Sub Main(args As String()) Dim context As New CrmDemoDataContext() With { _ .Log = Console.Out _ } Dim product As Product = _ context.Products.Where(Function(p) p.ProductID = 7219).SingleOrDefault() ' Ensure that the product was retrieved. If product Is Nothing Then Return End If DeleteProduct(context.Connection, 7219) context.Products.DeleteOnSubmit(product) Try context.SubmitChanges() Catch ex As ChangeConflictException For Each changeConflict As ObjectChangeConflict In context.ChangeConflicts changeConflict.Resolve(RefreshMode.KeepChanges, True) Next context.SubmitChanges() End Try End Sub |
Trying to resolve such a conflict with just specifying the refresh mode will fail (with InvalidOperationException having text "Error refreshing a deleted object."), as no changes in the values of update check parameters can make valid the WHERE statement being used. Another argument that the Resolve method may take is an 'autoResolveDeletes' boolean. When this argument is set to true, the context ensures that the entities corresponding to the deleted rows are wiped out from the cache and no more participate in submitting changes. After that, we again call the SubmitChanges() method, though in this particular situation it will do nothing: as someone else removed the row corresponding to the entity we are working with, there is no need in additional SQL commands.