Well known TransactionScope
Since version 2 of .NET Framework every developer has a great tool to handle different types of “transactions” (transaction processing, or business transaction is information processing that is divided into individual, indivisible operations called transactions. Each transaction must succeed or fail as a complete unit. It can never be only partially complete). But often the power of the TransactionScope mechanism is underestimated. The most frequently used case is to just wrap Entity Framework database context by TransactionScope. Then in case of an error or exception just revert all the changes have been done inside the transaction. But .NET Framework offer a lot more when you dig deep into the MSDN documentation.
The TransactionScope is only a part of a toolset which can do “transactions magic” for you. Let us imagine such a business case (which could be typical real world application logic, not just a theoretical example):
Multi element transaction
A services layer provides a method which covers a business scenario “create a Product”. The high level flow is:
- Create product instance in database
- Create thumbnail picture of product
Above 2 steps from technical point of view are totally different, but the goal here is that they complete business case “creation of Product” as a not separable unit. So if one of them fails then effects of others should be somehow reverted.
Going into implementation details, the flow is (and possible actions that need to be taken in case of revert):
- Add instance of Product to EntityFramework context and save (if fail – revert transaction on connected database)
- Create and save thumbnail on disk (if fail –remove file from disk)
Usage of TransactionScope
Those 2 steps we will enclose in single TransactionScope in ProductService method:
public void CreateProduct(string name) { var product = new Product() { Name = name }; using (var transaction = new TransactionScope()) { using (var context = new ProductsContext()) { context.Products.Add(product); context.SaveChanges(); } var thumbnailCreator = new ThumbnailCreator(product); thumbnailCreator.CreateThumbnail(); transaction.Complete(); } }
It is important to remember to call Complete() on TransactionScope, since default behavior is to rollback transaction when it gets disposed.
By default in all versions of Entity Framework, whenever you execute SaveChanges() to insert, update or delete on the database the framework will wrap that operation in a transaction. This transaction lasts only long enough to execute the operation and then completes. When you execute another such operation a new transaction is started. However, some users require greater control over their transactions, then the TransactionScope comes.
In the first step the Product entity is created and added to EF context. Then SaveChanges() method would immediately save it to the database. But in this case EF is “magically” aware of TransactionScope being opened earlier. So the internal database mechanism will start the transaction, handle proper isolation level and wait for commit or rollback.
But how about the next lines? They are also a part of “transaction” in the business meaning, but have nothing to do with the database. How to make them also aware of TransactionScope running?
IEnlistmentNotification interface
For this purpose, .NET Framework has IEnlistmentNotification interface to implement. It has 4 methods to implement:
void Commit(Enlistment enlistment); void InDoubt(Enlistment enlistment); void Prepare(PreparingEnlistment preparingEnlistment); void Rollback(Enlistment enlistment); Let us quickly look at the way ThumbnailCreator implements it: public void Prepare(PreparingEnlistment preparingEnlistment) { preparingEnlistment.Prepared(); } public void Commit(Enlistment enlistment) { enlistment.Done(); } public void Rollback(Enlistment enlistment) { try { var thumbnailDirectoryPath = GetThumbnailDirectoryPath(); Console.WriteLine("Deleted file {0}", Path.Combine(thumbnailDirectoryPath, "thumb.jpg")); } finally { enlistment.Done(); } } public void InDoubt(Enlistment enlistment) { enlistment.Done(); }
Methods Prepare and InDoubt and Prepare can be used for “Two-phase commit protocol” implementations (more details: https://en.wikipedia.org/wiki/Two-phase_commit_protocol). For simple example let us focus on Commit and Rollback. Both have parameter of type Enlistment, which is used to mark this object as “Done” inside the transaction.
So the first step to implement a custom transaction component is to implement proper logic for IEnlistmentNotification methods.
Enlist your class to transaction
The second important part of the package is actually enlist your object to .NET transactions mechanism. The framework exposes a statically available field in System.Transactions namespace.
.NET Framework guarantees that if you access field Transaction.Current it will reference to the current transaction inside which the code is called. If no transaction is taking place, then this field will be null.
The EnlistVolatile is the method we would use to “connect” ThumbnailCreator object to the transaction.
public ThumbnailCreator(Product product) { _product = product; if (Transaction.Current != null) { Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None); } }
For testing purposes the method that creates thumbnail is simple, it will just show a message on the console:
public void CreateThumbnail() { var thumbnailDirectoryPath = GetThumbnailDirectoryPath(); Console.WriteLine("Deleted file {0}", Path.Combine(thumbnailDirectoryPath, "thumb.jpg")); }
Happy flow
Here you have a simple app that will help us test ProductsService:
static void Main(string[] args) { WriteProductsCount(); var service = new ProductService(); try { service.CreateProduct("beer"); } catch { } WriteProductsCount(); Console.ReadLine(); } private static void WriteProductsCount() { using (var context = new ProductsContext()) { Console.WriteLine("Number of products in db: {0}", context.Products.Count()); } }
In the first case nothing goes wrong, so the result is as expected:
Do not worry about failures
But then we would simulate that something is wrong by adding Exception during the transaction:
public void CreateProduct(string name) { var product = new Product() { Name = name }; using (var transaction = new TransactionScope()) { using (var context = new ProductsContext()) { context.Products.Add(product); context.SaveChanges(); } var thumbnailCreator = new ThumbnailCreator(product); thumbnailCreator.CreateThumbnail(); throw new Exception(); transaction.Complete(); } }
In such a case the Complete() method is never called. So a default behavior is to Rollback whole transaction.
As soon as Exception occurred inside the transaction, the whole scope is interrupted. Consequently, all objects that implement IEnlistmentNotification and are enlisted to the transaction will be notified and do Rollback. In this case the thumbnail file gets removed.
Also Entity Framework automatically enlists to the transaction opened within TransactionScope, therefore no new element in the database is created.
TransactionScope and IEnlistmentNotification set is an elegant way to handle transactions. Moreover, it is available in the framework. You can use it to easily extend existing code inside TransactionScope by new elements. Instead of trying to write own transactions mechanisms for each element, it is nice to have one abstract that is provided by the framework and available everywhere. It is worth using, not only for databases but also to cover filesystems, API calls etc.
Below you will find a list of typical cases in which you can consider TransactionScope and IEnlistmentNotification as a useful pair:
- Cover with transaction multiple EF context instances
- Compose some database operations and filesystem operations to one atomic unit
- When want to do multiple API calls (like “ADD”) in atomic way, then in rollback you can have “DELETE” calls
- In disturbed systems when a set of service layer calls is considering as one “business” transaction
The above list contains only few examples. For sure there are lot more where you will find it useful.
Note about Entity Framework database connection
Default behavior when Entity Framework creates a connection to the database is to enlist to the existing transaction. If you want to control it yourself, you need to add Enlist=false to connection string.
Great read! I have been searching for how to extend TransactionScope participation for other resources (i.e. FileSystem) so operations command operations can be treated atomically.
Very well explained. Thanks
Excellent example that ties into business world scenarios. Assuming this is a non-EntityFramework (straight-up SQL Server) implementation, will the database/table involved in an underlying stored procedure be locked during the course of this TransactionScope? The primary table involved is very busy and needs to be highly-available.
If I make the database call and then want to send a response message to a third-party, but the third-party is not available to receive my message, I want to rollback the entire transaction. These are my business requirements…
the locking is usually done by database provider implementation. So it depends on database technology and isolation level.
You can think about lowering the level of isolation for critical read calls or consider some caching.