Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF eqivalent for rows affected of SqlCommand.ExecuteNonQuery

In an approval workflow I want to ensure that reminder emails are sent exactly once.

With SqlCommand.ExecuteNonQuery I can ensure this by testing the return value. What is the recommended solution using EF? According to the documentation ObjectContext.SaveChanges does not return an equivalent value.

SqlCommand example: (The TransactionScope is used to rollback the DB update in case SendMail fails.)


Dim sql = "UPDATE LeaveApprovalRequests SET State = 'Reminded'" &
          " WHERE ID=3 AND State <>'Reminded'"
Using scope As New TransactionScope
    Using cnx As New SqlConnection(My.Settings.connectionString)
        cnx.Open()
        Dim cmd As New SqlCommand(sql, cnx)
        If 1 = cmd.ExecuteNonQuery Then
             SendMail()
        End If
        scope.Complete()
    End Using
End Using

By enabling optimistic concurrency (using ConcurrencyMode=Fixed on a RowVersion property) and catching the OptimisticConcurrencyException I am able to identify if the object was actually updated in the store. Now the TransactionScope (used to rollback the DB update if SendMail fails) throws a deadlock error. Why?


Using scope As New TransactionScope
  Using ctx As New ApprovalEntities
    Try
      Dim approval = ctx.LeaveApprovalRequests.
        Where(Function(r) r.ID = 3 And r.State = "Created"
        ).FirstOrDefault
      If approval Is Nothing Then
        Console.WriteLine("not found")
        Exit Sub
      End If
      Threading.Thread.Sleep(4000)
      approval.State = "Reminded"
      ctx.SaveChanges()
      SendMail()
    Catch ex As OptimisticConcurrencyException
      Exit Try
    End Try
  End Using
  scope.Complete()
End Using

like image 391
Peter Meinl Avatar asked Nov 06 '22 08:11

Peter Meinl


1 Answers

Well, as a matter of fact the exact number of rows affected can be inferred from a call to ObjectContext.SaveChanges().

If you take a look at the ObjectContext.SaveChanges documentation you'll see:

public int SaveChanges()

  1. Return Value: The number of objects in an Added, Modified, or Deleted state when SaveChanges was called.
  2. "SaveChanges operates within a transaction. SaveChanges will roll back that transaction and throw an exception if any of the dirty ObjectStateEntry objects cannot be persisted."

(1) and (2) basically means that if your call to SaveChanges() has been successfully completed and you don't get any exception then EF guarantees that the return value exactly reflects the number of objects that has been modified.

Therefore, all you need to do is:

try {
    // Try to save changes, which may cause a conflict.
    int num = context.SaveChanges();
    SendMail();
}
catch (OptimisticConcurrencyException) {
    //Refresh the entity, using ClientWins
    context.Refresh(RefreshMode.ClientWins, yourEntityObject);
    //SaveChanges again;
    context.SaveChanges();
}
When Refresh with ClientWins is called, it executes a query to retrieve the current values of this entity in the database, including the new timestamp. Therefore, all of the original field values have been updated to reflect the latest database values so we can safely try SaveChanges() one more time.

Updated With Your Question:
The task is: only send an email if I am able to change the state from created to reminded. Thus it does not make sense to force through the SaveChanges when handling the OptimisticConcurrencyException. The error handler should exit if changing the state caused the exception and otherwise retry the whole task (reading and saving). How can I do this if optimistic concurreny is enabled via a RowVersion column and not by state only?

Well, each time a row is modified, the rowversion field is automatically updated, so in your case that would the best if you turn off Concurrency Mode on rowversion and turn it on, for the State property, so that your code will be as simple as:
try {
    context.SaveChanges();
    SendMail();
}
catch (OptimisticConcurrencyException) {
    // RowVersion Concurrency Mode = Fixed 
    // State Concurrency Mode = None
    // If it reaches here it means that the State has been changed;
    // so you do nothing except than throwing the exception
    throw;
}

But, if you would like to set Concurrency Mode = Fixed only for the rowversion property (like you mentioned), then it means that you can potentially get OptimisticConcurrencyException for change on any field including State, so it will be a little bit more work do the job:
try {
    ctx.SaveChanges();
    SendMail;
}
catch (OptimisticConcurrencyException) {
    // RowVersion Concurrency Mode = Fixed 
    // State Concurrency Mode = None
    // If it reches here it means that ANY/All field(s) has changed
    // So we need to see if it was State:
    ctx.Refresh(RefreshMode.ClientWins, approval);
    ObjectStateEntry ose = ctx.ObjectStateManager.GetObjectStateEntry(approval);
    string stateValue = ose.OriginalValues["State"].ToString();
    // If the value is still "Created" then something else should have changed,
    // And caused the exception, so we can proceed with the Save:
    if (stateValue == "Created") {
        ctx.SaveChanges();
        SendMail;
    }
    else {
        // Nope, it was state, so we throw the exception to the caller:
        throw;
    }

like image 143
Morteza Manavi Avatar answered Nov 09 '22 08:11

Morteza Manavi