Entity Framework 6 Asynchronous Operations and TranscationScope

I am doing a search on stackoverflow but cannot find a similar question, please indicate if there is one.

I tried to implement a universal reusable repository with synchronous and asynchronous operations, but with my little knowledge with Entity Framework and Unit Of Work. I try my best to find the right way to implement it.

I added some changes to the SaveAndCommit operation, but I don’t know how best to do this with transaction and asynchronism.

---- Editing ----

According to my understanding, transactions should be used when several operations are performed, but for the purposes of understanding, I used it for one operation. (Please correct me if I am 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 advise and suggest the best way to achieve this.


Now it seems that the correct way to implement a transaction scope with an asynchronous 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 } } 

Links MSDN Link

A clearer blog

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

+6
source share
1 answer

Edit:

In order for transaction areas to work with async-await starting with .NET 4.5.1, you can pass the TransactionScopeAsyncFlowOption.Enabled flag to its constructor:

 using (var scope = new TransactionScope(... , TransactionScopeAsyncFlowOption.Enabled)) 

This ensures that transaction areas behave well with continuation. See Get TransactionScope for working with async / await for more details.

Note. This feature has been available since .NET 4.5.1.

Edit 2:

Well, after @Jcl's comment on BeingTransaction I searched and found this answer :

With the introduction of EF6, Microsoft recommends using the new API methods: Database.BeginTransaction() and Database.UseTransaction() . System.Transactions.TransactionScope is just the old style of writing transactional code.

But Database.BeginTransaction() is only used for database-related transaction operations , while System.Transactions.TransactionScope makes "simple C # code" also transactional .

Limitations of the new asynchronous TransactionScope functions:

  • To work with asynchronous methods, .NET 4.5.1 or more is required.

  • It cannot be used in cloud scenarios unless you are sure that you have one and only one connection (cloud scripts do not support distributed transactions).

  • It cannot be combined with the Database.UseTransaction() approach of the previous sections.

  • It will throw exceptions if you throw any kind of DDL (for example, due to a database initializer) and did not enable distributed transactions
    through the MSDTC service.

It seems like a new approach starting with EF6 and above should use Database.BeginTransaction() instead of TransactionScope , given the limitations.

Finally:

This is the correct way to record transaction calls with an asynchronous transaction:

 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 if your area is wrapped in a using statement, as this will take a rollback if the commit failed.

Related question: Rollback transaction Entity Framework 6

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) { } } 

Do not do what you think is doing. When you execute an asynchronous method, you usually should wait for it asynchronously using the await keyword. This method:

  • Uses void as the return type. If it is an asynchronous API, it must be at least async Task . async void methods are intended only for event handlers, where this is clearly not the case.
  • The end user is likely to expect this method, it should be converted to:

     public virtual Task SaveAndCommitAsync(TEntity entity) { try { Save(entity); return _unitOfWork.SaveChangesAsync(); } catch (DbEntityValidationException e) { } } 

If you want to include a transaction scope, this method should be expected:

 public virtual async Task SaveAndCommitAsync(TEntity entity) { try { Save(entity); await _unitOfWork.SaveChangesAsync(); } catch (DbEntityValidationException e) { } } 

The same goes for the rest of your asynchronous methods. Once the transaction exists, make sure you expect this method.

Also, do not swallow such exceptions, do something useful with them, or simply do not catch.

+18
source

Source: https://habr.com/ru/post/981505/


All Articles