LinqConnect Documentation
Concurrency Conflicts - ChangeConflictException

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.

Processing Concurrency Conflicts

Here is a code sample that should result in throwing ChangeConflictException:


C#csharpCopy 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 BasicCopy 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#csharpCopy 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 BasicCopy 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#csharpCopy 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 BasicCopy 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.

Concurrency Conflicts Involving Deletions

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:

  1. 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#csharpCopy 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 BasicCopy 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.

  2. Updating a deleted entity. I.e., someone else deleted the entity you are trying to update; the only way to synchronize the context and database is to detach the deleted entity from the context. In this case, you can use the 'autoResolveDeletes' parameter of Resolve as well. If this parameter is true, the entity is removed from the cache on the Resolve call.
  3. Deleting an updated entity. Here the behaviour is the same as in the situation with two updates: we resolve the conflict and submit changes. The only thing is that the RefreshMode policy becomes irrelevant: the 'original' values are taken from the database, the 'current' values may have whatever value (since the row will be deleted anyway).