Entity Framework 6 operaciones asíncronas y TranscationScope

Busco en stackoverflow pero no pude encontrar una pregunta similar, indíqueme si ya hay una.

Estaba tratando de implementar un repository genérico reutilizable con operaciones tanto de sincronización como asincrónicas, pero con mi poco conocimiento de Entity Framework y Unit of Work, estoy luchando por encontrar la manera correcta de implementarlo.

He agregado algunas variaciones en la operación SaveAndCommit pero no sé cuál es la mejor manera de hacerlo con transacciones y asíncronos.

—-Editar—-

Según mi entendimiento, las transacciones deben usarse cuando se realizan más de una operación, pero para propósitos de comprensión, la usé para una operación. (Por favor corrígeme si estoy equivocado)

Esto es lo que he hecho hasta ahora.

public class Service : IService where TEntity : Entity { #region Constructor and Properties UnitOfWork _unitOfWork { get { return UnitOfWork.UnitOfWorkPerHttpRequest; } } protected DbSet Entities { get { return _unitOfWork.Set(); } } #endregion Constructor and Properties #region Operations public virtual IQueryable QueryableEntities() { return Entities; } public virtual async Task<IList> WhereAsync(Expression<Func> predicate) { return await Entities.Where(predicate).ToListAsync(); } public virtual IList Where(Expression<Func> predicate) { return Entities.Where(predicate).ToList(); } public virtual async Task FirstOrDefaultAsync(Expression<Func> predicate) { return await Entities.FirstOrDefaultAsync(predicate); } public virtual TEntity FirstOrDefault(Expression<Func> predicate) { return Entities.FirstOrDefault(predicate); } public virtual async Task 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 CountAsync(Expression<Func> predicate = null) { if (predicate != null) { return await Entities.CountAsync(predicate); } return await Entities.CountAsync(); } #endregion Operations } 

Por favor guíame y sugiere la mejor manera de lograrlo.


Ahora parece que la forma correcta de implementar un ámbito de transacción con una llamada asíncrona sería

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

Referencias de referencia de MSDN

Blog con una descripción más clara.

visualstudiomagazine.com Para: cuando llama a SaveChanges, ninguno de sus cambios tendrá efecto hasta que llame al método Commit del objeto Transacción

Editar:

Para que los ámbitos de transacción funcionen junto con async-await , a partir de .NET 4.5.1 puede pasar un indicador TransactionScopeAsyncFlowOption.Enabled a su constructor:

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

Esto asegura que los ámbitos de transacción se comporten bien con las continuaciones. Consulte Obtener TransactionScope para trabajar con async / await para obtener más información.

Tenga en cuenta que esta función está disponible desde .NET 4.5.1 en adelante.

Edición 2:

Bien, después de que @Jcl comentó sobre BeingTransaction , busqué y encontré esta respuesta :

Con la introducción de EF6, Microsoft recomienda utilizar nuevos métodos de API : Database.BeginTransaction() y Database.UseTransaction() . System.Transactions.TransactionScope es un estilo antiguo de escritura de código transaccional.

Pero Database.BeginTransaction() se usa solo para transacciones de operaciones relacionadas con la base de datos , mientras que System.Transactions.TransactionScope hace que el posible ‘código C # plano’ también sea transaccional .

Limitaciones de las nuevas características asíncronas de TransactionScope :

  • Requiere .NET 4.5.1 o superior para trabajar con métodos asíncronos.

  • No se puede usar en escenarios en la nube a menos que esté seguro de tener una y solo una conexión (los escenarios en la nube no son compatibles con la distribución
    actas).

  • No se puede combinar con el enfoque Database.UseTransaction() de las secciones anteriores.

  • Lanzará excepciones si emite un DDL (por ejemplo, debido a un
    Inicializador de base de datos) y no han habilitado transacciones distribuidas
    a través del servicio MSDTC.

Parece que el nuevo enfoque a partir de EF6 y superior es usar Database.BeginTransaction() lugar de TransactionScope , dadas las limitaciones.

Para concluir:

Esta es la forma correcta de escribir llamadas de base de datos de transacciones asíncronas con ámbito:

 public virtual async Task SaveAndCommitWithTransactionAsync(TEntity entity) { using (var transaction = _unitOfWork.BeginTransaction()) { try { Save(entity); await _unitOfWork.SaveChangesAsync(); transaction.Commit(); } catch (DbEntityValidationException e) { } } } 

Tenga en cuenta que no se debe llamar a transaction.RollBack() en caso de que su scope esté envuelto en una statement de using , ya que tomará de la reversión si la confirmación no tuvo éxito.

Una pregunta relacionada: Entity Framework 6 transacción rollback

Este artículo relacionado arroja más luz sobre la nueva API

Nota al margen:

Esta pieza de código:

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

No está haciendo lo que usted cree que está haciendo. Cuando ejecuta un método que es asíncrono, generalmente debe esperar asincrónicamente usando la palabra clave await . Este método:

  1. Está utilizando void como su tipo de retorno. Si esta es una API asíncrona, debe ser al menos una async Task . async void métodos async void son solo para manejadores de eventos , donde claramente no es el caso aquí.
  2. El usuario final probablemente estará esperando este método, se debe convertir en:

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

Si desea incluir un scope de transacción , debe esperar este método:

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

Lo mismo ocurre con el rest de sus métodos asíncronos. Una vez que haya una transacción, asegúrese de esperar en el método.

Además, no trague excepciones como esa, haga algo útil con ellas o simplemente no las atrape.