Como implementar o padrão Unit Of Work com o Dapper?

Atualmente, estou tentando usar o Dapper ORM com o padrão Unit Of Work + Repository.

Eu quero usar a unidade de trabalho em oposição a um Repositório dapper simples devido ao fato de que minha inserção e atualizações exigem um grau de processamento de transação. Não consegui encontrar nenhum exemplo útil, pois a maioria parece usar o Entity Framework e ter um problema de vazamento dentro da Unidade de Trabalho.

Alguém pode por favor me dizer a direção correta?

   

    Este projeto do Git é muito útil. Eu comecei do mesmo e fiz algumas mudanças conforme minha necessidade.

    public sealed class DalSession : IDisposable { public DalSession() { _connection = new OleDbConnection(DalCommon.ConnectionString); _connection.Open(); _unitOfWork = new UnitOfWork(_connection); } IDbConnection _connection = null; UnitOfWork _unitOfWork = null; public UnitOfWork UnitOfWork { get { return _unitOfWork; } } public void Dispose() { _unitOfWork.Dispose(); _connection.Dispose(); } } public sealed class UnitOfWork : IUnitOfWork { internal UnitOfWork(IDbConnection connection) { _id = Guid.NewGuid(); _connection = connection; } IDbConnection _connection = null; IDbTransaction _transaction = null; Guid _id = Guid.Empty; IDbConnection IUnitOfWork.Connection { get { return _connection; } } IDbTransaction IUnitOfWork.Transaction { get { return _transaction; } } Guid IUnitOfWork.Id { get { return _id; } } public void Begin() { _transaction = _connection.BeginTransaction(); } public void Commit() { _transaction.Commit(); Dispose(); } public void Rollback() { _transaction.Rollback(); Dispose(); } public void Dispose() { if(_transaction != null) _transaction.Dispose(); _transaction = null; } } interface IUnitOfWork : IDisposable { Guid Id { get; } IDbConnection Connection { get; } IDbTransaction Transaction { get; } void Begin(); void Commit(); void Rollback(); } 

    Agora, seus repositorys devem aceitar este UnitOfWork de alguma forma. Eu escolho Injeção de Dependência com Construtor.

     public sealed class MyRepository { public MyRepository(IUnitOfWork unitOfWork) { this.unitOfWork = unitOfWork; } IUnitOfWork unitOfWork = null; //You also need to handle other parameters like 'sql', 'param' ect. This is out of scope of this answer. public MyPoco Get() { return unitOfWork.Connection.Query(sql, param, unitOfWork.Transaction, .......); } public void Insert(MyPoco poco) { return unitOfWork.Connection.Execute(sql, param, unitOfWork.Transaction, .........); } } 

    E então você chama assim:

    Com transação:

     using(DalSession dalSession = new DalSession()) { UnitOfWork unitOfWork = dalSession.UnitOfWork; unitOfWork.Begin(); try { //Your database code here MyRepository myRepository = new MyRepository(unitOfWork); myRepository.Insert(myPoco); //You may create other repositories in similar way in same scope of UoW. unitOfWork.Commit(); } catch { unitOfWork.Rollback(); throw; } } 

    Sem Transação:

     using(DalSession dalSession = new DalSession()) { //Your database code here MyRepository myRepository = new MyRepository(dalSession.UnitOfWork);//UoW have no effect here as Begin() is not called. myRepository.Insert(myPoco); } 

    Por favor, note que UnitOfWork é mais do que DBTransaction.

    Mais detalhes sobre o Repositório no código acima podem ser encontrados aqui .

    Eu já postei este código aqui . Mas esta questão parece mais relevante para mim para este código; então estou postando novamente em vez de apenas link para a resposta original.

    Edit 2018-08-03: O comentário de Amit realmente me fez pensar, e me fez perceber que o repository não precisa, de fato, ser propriedades no próprio contexto. Mas, ao contrário, os repositorys podem ter uma dependência do contexto. Em vez de continuar a fazer alterações incrementais nos exemplos de código abaixo. Vou simplesmente referenciar um repository de gits que eu juntei para conter esse conceito.

    De pé sobre os ombros dos outros aqui.

    Considerando que esta resposta é superior na maioria das pesquisas do Google referentes a “dapper” e “unidade de trabalho”. Eu queria fornecer minha abordagem, que eu usei com grande efeito várias vezes agora.

    Usando um exemplo fictício (e excessivamente simplificado):

     public interface IUnitOfWorkFactory { UnitOfWork Create(); } public interface IDbContext { IProductRepository Product { get; set; } void Commit(); void Rollback(); } public interface IUnitOfWork { IDbTransaction Transaction { get;set; } void Commit(); void Rollback(); } public interface IProductRepository { Product Read(int id); } 

    Observe como nem IDbContext ou IUnitOfWorkFactory implementa IDisposable. Isto é propositalmente feito para evitar uma abstração gotejante . Em vez disso, a confiança está em Commit() / Rollback() para cuidar da limpeza e do descarte.

    Um par de pontos antes de compartilhar implementações.

    • IUnitOfWorkFactory é responsável por instanciar o UnitOfWork e intermediar a conexão com o database.
    • IDbContext é o backbone do repository.
    • IUnitOfWork é um encapsulamento do IDbTransaction e garante que, ao trabalhar com vários repositorys, eles compartilhem um único contexto de database.

    Implementação do IUnitOfWorkFactory

     public class UnitOfWorkFactory : IUnitOfWorkFactory where TConnection : IDbConnection, new() { private string connectionString; public UnitOfWorkFactory(string connectionString) { if (string.IsNullOrWhiteSpace(connectionString)) { throw new ArgumentNullException("connectionString cannot be null"); } this.connectionString = connectionString; } public UnitOfWork Create() { return new UnitOfWork(CreateOpenConnection()); } private IDbConnection CreateOpenConnection() { var conn = new TConnection(); conn.ConnectionString = connectionString; try { if (conn.State != ConnectionState.Open) { conn.Open(); } } catch (Exception exception) { throw new Exception("An error occured while connecting to the database. See innerException for details.", exception); } return conn; } } 

    Implementação do IDbContext

     public class DbContext : IDbContext { private IUnitOfWorkFactory unitOfWorkFactory; private UnitOfWork unitOfWork; private IProductRepository product; public DbContext(IUnitOfWorkFactory unitOfWorkFactory) { this.unitOfWorkFactory = unitOfWorkFactory; } public ProductRepository Product => product ?? (product = new ProductRepository(UnitOfWork)); protected UnitOfWork UnitOfWork => unitOfWork ?? (unitOfWork = unitOfWorkFactory.Create()); public void Commit() { try { UnitOfWork.Commit(); } finally { Reset(); } } public void Rollback() { try { UnitOfWork.Rollback(); } finally { Reset(); } } private void Reset() { unitOfWork = null; product = null; } } 

    Implementação do IUnitOfWork

     public class UnitOfWork : IUnitOfWork { private IDbTransaction transaction; public UnitOfWork(IDbConnection connection) { transaction = connection.BeginTransaction(); } public IDbTransaction Transaction => transaction; public void Commit() { try { transaction.Commit(); transaction.Connection?.Close(); } catch { transaction.Rollback(); throw; } finally { transaction?.Dispose(); transaction.Connection?.Dispose(); transaction = null; } } public void Rollback() { try { transaction.Rollback(); transaction.Connection?.Close(); } catch { throw; } finally { transaction?.Dispose(); transaction.Connection?.Dispose(); transaction = null; } } } 

    Implementação do IProductRepository

     public class ProductRepository : IProductRepository { protected readonly IDbConnection connection; protected readonly IDbTransaction transaction; public ProductRepository(UnitOfWork unitOfWork) { connection = unitOfWork.Transaction.Connection; transaction = unitOfWork.Transaction; } public Product Read(int id) { return connection.QuerySingleOrDefault("select * from dbo.Product where Id = @id", new { id }, transaction: Transaction); } } 

    Para acessar o database, basta instanciar o DbContext ou injetar usando o contêiner IoC de sua escolha (eu pessoalmente uso o contêiner IoC fornecido pelo .NET Core ).

     var unitOfWorkFactory = new UnitOfWorkFactory("your connection string"); var db = new DbContext(unitOfWorkFactory); Product product = null; try { product = db.Product.Read(1); db.Commit(); } catch (SqlException ex) { //log exception db.Rollback(); } 

    A necessidade explícita de Commit() para essa operação de leitura simples parece excessiva, mas paga dividendos conforme o sistema cresce. E, aparentemente, oferece um benefício de desempenho menor, de acordo com Sam Saffron . Você “pode” também omitir o db.Commit() em operações de leitura simples, fazendo isso embora você deixe a conexão aberta e coloque o ônus de limpar as coisas no coletor de lixo. Então isso não é recomendado.

    Eu normalmente trago o DbContext para a dobra na camada de serviço, onde ele trabalha em uníssono com outros serviços para formar o “ServiceContext”. Em seguida, faço referência a esse ServiceContext na camada real do MVC. Como outro ponto de referência, recomenda-se usar async toda a pilha, se puder. É omitido aqui por simplicidade.