LinqConnect Documentation
In This Topic
    Concurrency Conflicts - ChangeConflictException
    In This Topic

    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).