Edit:
In order for transaction scopes to work together with async-await
, starting from .NET 4.5.1 you can pass in a TransactionScopeAsyncFlowOption.Enabled
flag to its constructor:
using (var scope = new TransactionScope(... ,
TransactionScopeAsyncFlowOption.Enabled))
This makes sure that the transaction scopes behaves nicely with continuations.
See Get TransactionScope to work with async / await for more.
Note this feature is available since .NET 4.5.1 onward.
Edit 2:
Okay, after @Jcl comment on BeingTransaction
, i searched and found this answer:
With the introduction of EF6, Microsoft recommends to use new API
methods: Database.BeginTransaction()
and Database.UseTransaction()
.
System.Transactions.TransactionScope is just old style of writing
transactional code.
But Database.BeginTransaction()
is used only for database related
operations transaction, whereas System.Transactions.TransactionScope
makes the possible 'plain C# code' also transactional.
Limitations of new asynchronous features of TransactionScope
:
Requires .NET 4.5.1 or greater to work with asynchronous methods.
It cannot be used in cloud scenarios unless you are sure you have one
and only one connection (cloud scenarios do not support distributed
transactions).
It cannot be combined with the Database.UseTransaction()
approach of
the previous sections.
It will throw exceptions if you issue any DDL (e.g. because of a
Database Initializer) and have not enabled distributed transactions
through the MSDTC Service.
It seems like the new approach starting EF6 and above is to use Database.BeginTransaction()
instead of TransactionScope
, given the limitations.
To conclude:
This is the proper way to write async transaction scoped db calls:
public virtual async Task SaveAndCommitWithTransactionAsync(TEntity entity)
{
using (var transaction = _unitOfWork.BeginTransaction())
{
try
{
Save(entity);
await _unitOfWork.SaveChangesAsync();
transaction.Commit();
}
catch (DbEntityValidationException e)
{
}
}
}
Note that transaction.RollBack()
should not be called in case your scope is wrapped in a using
statement, as it will take of the rollback if the commit was unsuccessful.
A related question: Entity Framework 6 transaction rollback
This related article sheds more light on the new API
Side note:
This piece of code:
public virtual void SaveAndCommitAsync(TEntity entity)
{
try
{
Save(entity);
_unitOfWork.SaveChangesAsync();
}
catch (DbEntityValidationException e)
{
}
}
Isn't doing what you think it's doing. When you execute a method which is asynchronous, you should usually asynchronously wait on it using the await
keyword. This method:
- Is using
void
as its return type. If this is an asynchronous API, it needs to be at least async Task
. async void
methods are only ment for event handlers, where this clearly isn't the case here
The end user will probably be awaiting on this method, it should be turned into:
public virtual Task SaveAndCommitAsync(TEntity entity)
{
try
{
Save(entity);
return _unitOfWork.SaveChangesAsync();
}
catch (DbEntityValidationException e)
{
}
}
If you want to include a Transaction Scope, then this method must be awaited:
public virtual async Task SaveAndCommitAsync(TEntity entity)
{
try
{
Save(entity);
await _unitOfWork.SaveChangesAsync();
}
catch (DbEntityValidationException e)
{
}
}
Same goes for the rest of your asynchronous methods. Once a transaction is there, make sure you await on the method.
Also, don't swallow exceptions like that, do something useful with them, or simply don't catch.