One of the last things you want to get from your Commerce site is that the order data is gone. What can be more confused than if your log shows that the cart has been converted into a purchase order, you even got the PO number, but after that, the order disappears? It’s nowhere to be found, even if you look into database. It’s kind of magic, but not the kind of magic you would want to have.
But everything happens for a reason. And actually it’s with a good reason: data consistency.
Episerver Commerce has the concept of TransactionScope. Simply put, it allows two or more database operations to be done as atomic: Either all of them succeed, or all of them will revert back. If a TransactionScope contains 3 operations A, B, C, then even if A, B succeeded, but C is yet to commit, and something goes wrong, then A and B would be reverted.
Here’s an example of TransactionScope:
using (var scope = new Mediachase.Data.Provider.TransactionScope()) { OrderGroupWorkflowManager.RunWorkflow(Cart, OrderGroupWorkflowManager.CartCheckOutWorkflowName, true, dic); Cart.CustomerId = PrincipalInfo.CurrentPrincipal.GetContactId(); var po = Cart.SaveAsPurchaseOrder(); // Add note to purchaseOrder AddNoteToPurchaseOrder("New order placed by {0} in {1}", po, PrincipalInfo.CurrentPrincipal.Identity.Name, "Public site"); po.AcceptChanges(); // Remove old cart Cart.Delete(); Cart.AcceptChanges(); // Commit changes scope.Complete(); }
Here we are having three actions: saving a cart as a purchase order, add a note to the PO and save it again, then delete the cart. Until scope.Complete() is reached, all those changes are subjected to be reverted back. If AddNoteToPurchaseOrder fails and an exception is thrown, the first action will be reverted back. If Cart.AcceptChanges() fails, then the order and the note will be reverted back. scope.Complete() is very important, it’s like checkpoint in game – when you die before reaching a checkpoint, you’ll have to start at previous checkpoint. If something go wrong before you reach scope.Complete(), your database will what it was before the TransactionScope.
You can try to do a little experiment with TransactionScope. Put a breaking at Cart.Delete();, for example, then make sure the code stops there in the debugger. Now go to the database and try to find the newly created purchase order.
You just can’t – SQL Server tries to read, but actually it’s blocked. The level of transaction isolation of TransactionScope is Serialization, so other threads can’t read the data which are being manipulated by our thread. Of course if you write some code inside our TransactionScope to read the newly created PO, it should be fine, because it’s same thread. But other threads have to wait. Only when we leave out scope.Complete(), then SQL Server Management studio can complete the read and return some data to us!
It’s worth noting that TransactionScope support nested transactions. In fact, the above example is a nested transaction. There is already a TransactionScope inside Cart.SaveAsPurchaseOrder(). In case of nested transactions, the outer most transaction wins – only when it commits then all nested transactions commits.
TransactionScope is a great feature – some data is just so important that it better be consistent other than in faulty state. One of the places where you should use TransactionScope is like above – where you do the final steps to convert a cart into an order. However, think about what would you want to put in a TransactionScope – only the critical business which should go together (or revert together). As we mentioned about, TransactionScope will block other threads from doing certain things, so too big scopes can definitely hurt your performance.
The problem I mentioned in the beginning of this blogpost was actually caused by two reasons: the unawareness of TransactionScope and its behaviors, and the lack of proper logging when an exception is thrown. When everything was taken into account, it’s clear and reasonable of why did it happen.
This post hopefully clears any confusion regarding TransactionScope – and the latter – it deserves a post on it own. I’ll come back to you soon.
Great post!
Does the TransactionScope also take care of something like creating a new contact?
For example if I have this inside my scope:
var customerContact = CustomerContact.CreateInstance();
customerContact.FirstName = finalShippingAddress.FirstName;
customerContact.LastName = finalShippingAddress.LastName;
customerContact.Email = finalShippingAddress.Email;
customerContact.SaveChanges();
If the scope.Complete() does not exceute will this also revert my created contact?
For what I can see the SaveChanges() is using a TransactionScope of it own but maybe it will be a Rollback anyway?