Unit Of Work Pattern com Entity Framework

Hoje em dia fala-se muito a respeito de “Persistence Ignorance”, e, em volta deste assunto existem vários padrões. Um deles é o Unit Of Work, que é descrito pelo Martin Fowler da seguinte forma: “mantém uma lista de objetos afetados por uma transação comercial e coordena a gravação de alterações e a resolução de problemas de concorrência.”.

Este padrão é muito bem difundido, e já está presente em grande parte dos ORM`s. Podemos citar a classe DbContext no Entity Framework, a interface ITransaction no NHibernate e a classe DataContext do LINQ TO SQL e LINQConnect.

Logo, caso você adote alguma destas ferramentas de persistência de dados – o que é bem provável – não será necessário programar esse padrão do zero, apesar de que seja interessante termos nossa própria interface para o padrão Unit Of Work, encapsulando a implementação da ferramenta de persistência de dados. Mas porque isso ? Posso pensar em uma série de motivos para isso: talvez seja interessante programar alguma lógica específica para tratamento de erros, geração de log, controles de transação, etc. Mas o principal motivo é o fato de não acoplarmos a nossa aplicação a uma tecnologia especifica de persistência de dados, o que aumenta a testabilidade e ainda facilita a mudança de tecnologia de persistência de dados futuramente, caso haja necessidade.

Primeiro iremos definir a interface padrão para a nossa Unit Of Work.

public interface IUnitOfWork : IDisposable
{
    IQueryable<TEntity> GetQueryable<TEntity>() where TEntity : class;
 
    void Attach<TEntity>(TEntity item) where TEntity : class;
 
    void SetModified<TEntity>(TEntity item) where TEntity : class;
 
    void Commit();
 
    void CommitAndRefreshChanges();
 
    void RollbackChanges();
}



Para a implementação concreta da interface IUnitOfWork, teremos a classe EFUnitOfWork que irá utilizar o Entity Framework para a implementação concreta da persistência de dados.

public class EFUnitOfWork : DbContext, IUnitOfWork
{
    public IQueryable<TEntity> GetQueryable<TEntity>() where TEntity : class
    {
        return base.Set<TEntity>();
    }
 
    public void Attach<TEntity>(TEntity item) where TEntity : class
    {
        base.Set<TEntity>().Attach(item);
    }
 
    public void SetModified<TEntity>(TEntity item) where TEntity : class
    {
        base.Entry<TEntity>(item).State = System.Data.EntityState.Modified;
    }
 
    public void Commit()
    {
        base.SaveChanges();
    }
 
    public void CommitAndRefreshChanges()
    {
        bool saveFailed = false;
 
        do
        {
            try
            {
                base.SaveChanges();
 
            }
            catch (DbUpdateConcurrencyException ex)
            {
                saveFailed = true;
 
                ex.Entries.ToList()
                          .ForEach(entry =>
                          {
                              entry.OriginalValues.SetValues(entry.GetDatabaseValues());
                          });
            }
        } while (saveFailed);
    }
 
    public void RollbackChanges()
    {
        base.ChangeTracker.Entries()
                         .ToList()
                         .ForEach(entry => entry.State = System.Data.EntityState.Unchanged);
    }
}



Agora veremos um exemplo de utilização do UnitOfWork em um processo de negócio.

public sealed class ClienteDomainService
{
    private IUnitOfWork _unitOfWork;
 
    public ClienteDomainService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
 
    public void AtualizarClientes(IEnumerable<Cliente> clientes)
    {
        if (clientes == null)
            throw new ArgumentNullException("clientes");
 
        try
        {
            foreach (Cliente cliente in clientes)
            {
                //marca a instância da entidade como modificada
                _unitOfWork.SetModified<Cliente>(cliente);
            }
 
            //persist as alterações em banco de dados
            _unitOfWork.Commit();
        }
        catch (Exception)
        {
            //rollback caso ocorra algum erro
            _unitOfWork.RollbackChanges();
        }
    }
}



Note que toda implementação foi feita baseada em uma interface, ou seja, tanto faz a tecnologia de persitência de dados que está por trás.

Essa flexibilidade nos permite aumentar, em muito, a flexibilidade e testabilidade do sistema.

Na hora de executar os testes unitários posso facilmente substituir a implementação concreta da Unit of Work por uma implementação fake, coisa que não seria possível caso eu utilizasse diretamente uma classe concreta.
Outro ponto positivo é que desta forma fica mais fácil mudar de tecnologia de persistência de dados caso necessário.