Entity Framework 6 async operations and TranscationScope

14,360

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:

  1. 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
  2. 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.

Share:
14,360

Related videos on Youtube

Sandeep Kumar
Author by

Sandeep Kumar

Updated on September 15, 2022

Comments

  • Sandeep Kumar
    Sandeep Kumar over 1 year

    I search on stackoverflow but could not find a similar question, please point me if there is already one.

    I was trying to implement a generic reusable repository with both sync and async operations but with my little knowledge with Entity Framework and Unit Of Work I'm struggling to find the correct way to implement it.

    I have added some variations on SaveAndCommit operation but don't know what is the best way to do it with transaction and async.

    ----Edit----

    As per my understanding transactions should be used when more than one operations is performed but for understanding purposes I used it for one operation. (Please correct me if I'm wrong)

    This is what I have done so far

    public class Service<TEntity> : IService<TEntity>
        where TEntity : Entity
    {
        #region Constructor and Properties
    
        UnitOfWork _unitOfWork { get { return UnitOfWork.UnitOfWorkPerHttpRequest; } }
    
        protected DbSet<TEntity> Entities
        {
            get { return _unitOfWork.Set<TEntity>(); }
        }
    
        #endregion Constructor and Properties
    
    
        #region Operations
    
        public virtual IQueryable<TEntity> QueryableEntities()
        {
            return Entities;
        }
    
        public virtual async Task<IList<TEntity>> WhereAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return await Entities.Where(predicate).ToListAsync();
        }
    
        public virtual IList<TEntity> Where(Expression<Func<TEntity, bool>> predicate)
        {
            return Entities.Where(predicate).ToList();
        }
    
        public virtual async Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return await Entities.FirstOrDefaultAsync(predicate);
        }
    
        public virtual TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate)
        {
            return Entities.FirstOrDefault(predicate);
        }
    
        public virtual async Task<TEntity> GetByIdAsync(int id)
        {
            return await Entities.FindAsync(id);
        }
    
        public virtual TEntity GetById(int id)
        {
            return Entities.Find(id);
        }
    
        // Method to the change the EntityState
        public virtual void Save(TEntity entity)
        {
            if (entity.Id == 0)
            {
                Entities.Add(entity);
            }
            else
            {
                _unitOfWork.Entry(entity).State = EntityState.Modified;
            }
        }
    
        #region Need clarification here
    
        // Uses transaction scope to commit the entity and dispose automatically
        // call rollback but this is not async and don't have any async
        // functions (Or I could not find)
        public virtual void SaveAndCommit(TEntity entity)
        {
            using (var transaction = _unitOfWork.BeginTransaction())
            {
                try
                {
                    Save(entity);
                    transaction.Commit();
                }
                catch (DbEntityValidationException e)
                {
                }
            }
        }
    
        // This is asynchronous but don't uses transaction
        public virtual async Task SaveAndCommitAsync(TEntity entity)
        {
            try
            {
                Save(entity);
                await _unitOfWork.SaveChangesAsync();
            }
            catch (DbEntityValidationException e)
            {
            }
        }
    
        // Tried to mix async and transaction but don't know if it will actually         
        // work or correct way of doing this
        public virtual async Task SaveAndCommitWithTransactionAsync(TEntity entity)
        {
            using (var transaction = _unitOfWork.BeginTransaction())
            {
                try
                {
                    Save(entity);
                    await _unitOfWork.SaveChangesAsync();
                }
                catch (DbEntityValidationException e)
                {
                    transaction.Rollback();
                }
            }
        }
    
        #endregion Need clarification here
        
        public virtual async Task DeleteAsync(TEntity entity)
        {
            if (entity == null) return;
    
            Entities.Remove(entity);
            await _unitOfWork.SaveChangesAsync();
        }
    
        //All similar methods for delete as for Save
    
        public virtual async Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate = null)
        {
            if (predicate != null)
            {
                return await Entities.CountAsync(predicate);
            }
    
            return await Entities.CountAsync();
        }
    
        #endregion Operations
    
    }
    

    Please guide me and suggest the best way to achieve this.


    Now it seems that the correct way to implement a transaction scope with async call would be

    public virtual async Task SaveAndCommitWithTransactionAsync(TEntity entity)
        {
            using (var transaction = _unitOfWork.BeginTransaction())
            {
                    Save(entity);
                    await _unitOfWork.SaveChangesAsync();
    
                    // Still no changes made to database
                    transaction.Commit();
                
                   //Rollback will automatically be called by using in dispose method
            }
        }
    

    References MSDN Reference

    Blog with more clear description

    visualstudiomagazine.com For : when you call SaveChanges, none of your changes will take effect until you call the Transaction object's Commit method

  • Flexo
    Flexo over 9 years
    Comments are not for extended discussion; this conversation has been moved to chat.
  • MikeJansen
    MikeJansen over 4 years
    Note that the commit is not asynchronous, not even the commit of the implicit transaction created during the SaveChangesAsync call. So TransactionScopeAsyncFlowOption.Enabled allows a TransactionScope to work with async (meaning the transaction follows the async continuations), but the commit itself is not async. It blocks a system thread for the duration. Same applies to DbContextTransaction.Commit. All paths end up calling DbTransaction.Commit which is synchronous.