É um dos padrões de design mais comuns nas empresas de desenvolvimento de software. De acordo com Martin Fowler, o padrão de unidade de trabalho "mantém uma lista de objetos afetados por uma transação e coordena a escrita de mudanças e trata possíveis problemas de concorrência".

O padrão de unidade de trabalho não é necessariamente algo que você vai implementar explicitamente. A interface ITransaction em NHibernate, a classe DataContext no LINQ to SQL, e a classe ObjectContext no Entity Framework, são exemplos de uma Unidade de Trabalho.

Porém existem várias razões para implementar sua própria unidade de trabalho como efetuar logs, tracing, gerenciar as transações, promover testabilidade em seu sistema, entre outras.

Vamos criar um contexto que gerenciará os Articles (artigos), implementando as seguintes interfaces e classes, levando em conta que teremos um repositório de artigos.

public interface IArticleRepository : IDisposable
{
        IEnumerable<Article> GetArticles();
        Article GetArticleByID(int articleId);
        void Add(Article article);
        void Delete(int articleId);
} 
Listagem 1. Interface IArticleRepository
public class ArticleRepository : IArticleRepository
{
    private readonly ArticleContext _context;

    public ArticleRepository(ArticleContext articleContext)
    {
        _context = articleContext;
    }

    public IEnumerable<Model.Article> GetArticles()
    {
        return _context.Articles;
    }

    public Model.Article GetArticleByID(int articleID)
    {
        return _context.Articles.FirstOrDefault(x => x.ArticleId == articleID);
    }

    public void Add(Article article)
    {
        _context.Articles.Add(article);
    }

    public void Delete(int articleId)
    {
        var article = _context.Articles.FirstOrDefault(x => x.ArticleId == articleId);
        _context.Articles.Remove(article);
    }

    public void Dispose()
    {
        if (_context != null)
        {
            _context.Dispose();
        }
        GC.SuppressFinalize(this);
    }
}
Listagem 2. Implementação da Interface IArticleRepository
public interface IUnitOfWork
{
        IArticleRepository Articles { get; }   
        void Commit();
} 
Listagem 3. Interface UnitOfWork
 
public class UnitOfWork : IUnitOfWork
{
    private IArticleRepository _articles;

    private readonly ArticleContext _articleContext;

    public UnitOfWork(ArticleContext articleContext)
    {
        _articleContext = articleContext;
    }

    public IArticleRepository Articles
    {
        get
        {
            if (_articles == null)
            {
                _articles = new ArticleRepository(_articleContext);
            }
            return _articles;
        }
    }

    public void Commit()
    {
        _articleContext.SaveChanges();
    }
}
Listagem 4. Implementação da interface UnitOfWork

Repare que no construtor da unidade de trabalho passamos o contexto como parâmetro, e o mesmo objeto é passado para o repositório garantindo que o repositório e a unidade trabalho trabalhem com o mesmo contexto. Quando estiver trabalhando com sistemas complexos você terá N repositórios compartilhando o mesmo contexto, que será responsável por fazer uma única transação no banco quando for necessário inserir, deletar ou atualizar os dados.

public class ArticleContext : DbContext
    {
        static ArticleContext()
        {
            Database.SetInitializer<ArticleContext>(
                new ContextInitialize());
        }

        public ArticleContext(string nameOrConnectionString) :
            base(nameOrConnectionString)
        {

        }

        public DbSet<Article> Articles { get; set; }
    }
Listagem 5. Contexto (DbContext)

No construtor do contexto, estamos configurando o Initializer do database para que ele delete e crie o nosso banco de dados usando uma classe customizada ContextInitialize que herda de DropCreateDatabaseAlways. Existem outras opções padrões como DropCreateDatabaseIfModelChanges e CreateDatabaseIfNotExists.

public class ContextInitialize : DropCreateDatabaseAlways<ArticleContext>
{
        protected override void Seed(ArticleContext context)
        {
            context.Articles.Add(new Article
            {
                Title = "Titulo Teste 1",
                Text = "Texto teste 1"
            });

            context.Articles.Add(new Article
            {
                Title = "Titulo Teste 2",
                Text = "Texto teste 2"
            });

            context.Articles.Add(new Article
            {
                Title = "Titulo Teste 3",
                Text = "Texto teste 3"
            });

            context.SaveChanges();
        }
}
Listagem 6. Inicializador do banco de dados
public class Article
{
        public int ArticleId { get; set; }
        public string Title { get; set; }
        public string Text { get; set; }
}
Listagem 7. Classe Article

O Entity Framework, por padrão, adota algumas convenções (Conventions) que ele usa para realizar algumas operações. Uma convenção é uma regra padrão pelo qual não teremos que fazer algumas configurações de mapeamento para nossas entidades, sendo que o Entity Framework vai realizá-las. Baseadas nessas convenções serão realizadas as tarefas de forma automática. No nosso exemplo o Entity Framework vai criar a tabela Article com os campos ArticleId (int, Chave primaria), Title (nvarchar(max))e Text (nvarchar(max)).

Observação: A classe DbContext por padrão vem com a propriedade AutoDetectChanges como true. Basicamente o AutoDetectChanges chama o método do contexto DetectChanges() sempre que houver algum evento no contexto. Desabilitando essa propriedade pode haver ganhos de desempenho.

De certa forma, você pode pensar na unidade de trabalho como um lugar que executará todo o código de manipulação de transação. As responsabilidades da Unidade de Trabalho são:

  1. Gerenciar as transações.
  2. Ordenar as inserções de banco de dados, exclusões e atualizações.
  3. Impedir duplicação de atualizações. Dentro de um único objeto de uma Unidade trabalho, diferentes partes do código pode marcar o mesmo objeto como alterado, mas a classe de unidade de trabalho só irá emitir um comando UPDATE no banco de dados.
[TestClass]
    public class UnitOfWorkTest
    {
        [TestMethod]
        public void UnitOfWork_Insert_New_Article()
        {
            var unitOfWork = new UnitOfWork(new ArticleContext("name=ArticleConnectionString"));
            var article = new Article
               {
                   Title = "Titulo do artigo 2",
                   Text = "Texto do artigo 2"
               };

            unitOfWork.Articles.Add(article);
            unitOfWork.Commit();
            Assert.AreNotEqual(article.ArticleId, 0);
        }

        [TestMethod]
        public void UnitOfWork_Update_Article_With_Articleid_Equals_2()
        {
            var unitOfWork = new UnitOfWork(new ArticleContext("name=ArticleConnectionString"));
            var article = unitOfWork.Articles.GetArticles().FirstOrDefault(x => x.ArticleId == 2);
            article.Title = "Titulo atualizado pela unidade de trabalho";
            unitOfWork.Commit();
            article = unitOfWork.Articles.GetArticles().FirstOrDefault(x => x.ArticleId == 2);
            Assert.AreEqual("Titulo atualizado pela unidade de trabalho", article.Title);
        }

        [TestMethod]
        public void UnitOfWork_Delete_Article()
        {
            var unitOfWork = new UnitOfWork(new ArticleContext("name=ArticleConnectionString"));
            var article = unitOfWork.Articles.GetArticles().FirstOrDefault();
            Assert.AreNotEqual(article.ArticleId, 0);
            unitOfWork.Articles.Delete(article.ArticleId);
            unitOfWork.Commit();
        }
    }
Listagem 8. Testes da unidade de trabalho
Nota: Para o código-fonte funcionar na sua máquina é necessário configurar a connection string no arquivo App.Config do projeto de teste.

O nosso exemplo serviu apenas para introduzir o assunto de unidade de trabalho, existem implementações bem mais complexas e mais robustas do que foi mostrado aqui.

Então finalizamos aqui este artigo, cujo objetivo é mostrar um dos padrões mais usados hoje em dia. Espero ter ajudado a todos, abraços e até a próxima oportunidade.