.NET

8 jul, 2016

.NET – Padrão Repository e Unit of Work com EF 6 (revisitado)

Publicidade

Neste artigo vou recordar os conceitos relacionados com o padrão Repositório e padrão Unit Of Work e sua implementação com o Entity Framework 6.x.

O padrão de projeto Repository acrescenta uma camada de abstração no topo da camada de consultas e ajuda eliminar a lógica duplicada na implementação do código de suas consultas ao modelo de entidades.

Foi Martin Fowler quem definiu o padrão Repository no seu livro – Patterns of Enterprise Application Architecture – da seguinte forma: “Intermedeia entre o domínio e as camadas de mapeamento de dados usando uma interface de coleção para acessar objetos de domínio” (numa tradução livre minha mesmo).

O que é o padrão Unit Of Work?

Unit Of Work ou Unidade de Trabalho é um padrão de projeto e, de acordo com Martin Fowler, o padrão de unidade de trabalho “mantém uma lista de objetos afetados por uma transação, coordena a escrita de mudanças e trata possíveis problemas de concorrência”.

Este padrão está presente em quase todas as ferramentas OR/M atuais (digo quase, pois não conheço todas) e geralmente você não terá que criar a sua implementação personalizada a menos que decida realmente fazer isso por uma questão de força maior.

Dessa forma, a interface ITransaction no NHibernate, a classe DataContext no LINQ to SQL e a classe DbSet no Entity Framwork são exemplos de implementações do padrão Unit of Work (até o famigerado DataSet  pode ser usado como uma Unit of Work).

Então, o padrão Unit of Work pode ser visto como um contexto, sessão ou objeto que acompanha as alterações das entidades de negócio durante uma transação, sendo também responsável pelo gerenciamento dos problemas de concorrência que podem ocorrer oriundos dessa transação.

Existem várias razões para implementar um repositório e a sua própria unidade de trabalho, como efetuar logs, tracing, gerenciar as transações, promover testabilidade em seu sistema, entre outras.

E, quando você for fazer isso, você deve estar certo de estar fazendo da forma correta.

Neste artigo, eu apresento como implementar o padrão Repository e Unit of Work de forma correta evitando muitos problemas em sua aplicação.

Recursos usados: Visual Studio Community 2015

Implementando o padrão Repositório: a forma ingênua

Você vai encontrar muitos artigos na web que tratam da implementação do padrão Repository. Uma boa parte deles utiliza a seguinte abordagem:

  • Definir uma interface para cada Repositório;
  • Fazer a implementação concreta do Repositório;
  • Injetar (via construtor) as implementações do Repositório concreto – (usando um Framework como o Ninject).

Para exemplificar, suponha que temos as entidades Cliente e Produto e a classe AppContexto, que representa o nosso contexto, em uma aplicação ASP .NET MVC (apenas para nos situarmos). Nessa abordagem temos:

1. Definição da interface para cada Repositório:

public interface IClienteRepository
{
   Cliente GetByID(int clienteID);
   Cliente GetTodos();
   void Salvar(Cliente cliente);
}

 

public interface IProdutoRepository
{
   Produto GetByID(int produtoID);
   Produto GetTodos();
   void Salvar(Produto produto);
}

2. Implementação concreta do Repositório e Injeção da dependência no construtor:

public class ClienteRepositorio : IClienteRepository
 {
     AppContexto _contexto = null;
        public ClienteRepositorio(AppContexto contexto)
        {
            _contexto = contexto;
        }

        public Cliente GetByID(int clienteID)
        {
            throw new NotImplementedException();
        }

        public Cliente GetTodos()
        {
            throw new NotImplementedException();
        }
        public void Salvar(Cliente cliente)
        {
            throw new NotImplementedException();
        }
 }

 

 public class ProdutoRepositorio : IProdutoRepository
  {
     AppContexto _contexto = null;
        public ProdutoRepositorio(AppContexto contexto)
        {
            _contexto = contexto;
        }
        public Produto GetByID(int produtoID)
        {
            throw new NotImplementedException();
        }
        public Produto GetTodos()
        {
            throw new NotImplementedException();
        }
        public void Salvar(Produto produto)
        {
            throw new NotImplementedException();
        }
    }

Essa implementação é funcional e aparentemente aderente às boas práticas, mas ela esconde alguns problemas.

  • Primeiro problema: se sua aplicação tiver (é quase certo que isso ocorra em uma aplicação de produção) muitas entidades relacionadas, você terá que definir uma interface e uma classe concreta por entidade para a qual deseja definir o seu repositório.
  • Segundo problema: isso fará com que você tenha código duplicado espalhado pela sua aplicação.
  • Terceiro problema: quando você injeta cada repositório individualmente, em cada controlador, você acaba criando múltiplas instâncias do seu contexto (leia-se DbContext), o que pode causar problemas de concorrência.

Em suma, cada repositório vai precisar criar uma instância do contexto (DbContext no nosso caso, que estamos usando o Entity Framework) para poder interagir com o banco de dados e isso pode trazer problemas de acesso concorrente aos dados.

Uma forma de contornar os problemas e criar um repositório genérico e implementar o padrão Unit of Work.

Na Figura 1 temos o problema do código duplicado na abordagem 1 interface/1 classe concreta, já na Figura 2, a abordagem do Repositório Genérico:

net_uow10
Figura 01
Figura 02
Figura 02

Na Figura 3 temos o problema das múltiplas instâncias do DbContext. Na Figura 4, temos a adoção do padrão Unit Of Work para resolver o problema:

Figura 03
Figura 03
Figura 04
Figura 04

Em uma aplicação ASP .NET MVC usando Entity Framework, geralmente os repositórios serão injetados em cada controlador, o que acaba anulando os benefícios da Unit of Work, pois para cada request específico, cada chamada a um método, o Action do Controlador acessa múltiplos repositórios e acaba criando múltiplas instâncias do Unit of Work; o que acaba chamando o método SaveChanges (Commit) múltiplas vezes; o que causa muitas idas e vindas ao seu banco de dados através de múltiplas transações – e esse não é o comportamento esperado.

Nota: A Microsoft define o DbContext assim: “Representa uma combinação dos padrões Unit-of-Work e Repositório que permite consultar o banco de dados e agrupar as alterações como uma unidade”.

Implementando o padrão Repositório: a forma mais correta

Pois bem, para resolver esses problemas temos que adotar uma abordagem mais acurada que envolve a criação de um repositório genérico para evitar a duplicação de código e a implementação do padrão Unit Of Work para garantir que todos os repositórios utilizem a mesma instância do contexto.

Para concluir a primeira parte deste artigo, vou apresentar uma implementação para o Repositório Genérico chamada IRepositorio<T> onde T representa uma classe/entidade do domínio da aplicação:

public interface IRepositorio<T> where T : class
    {
        IEnumerable<T> GetTudo(Expression<Func<T, bool>> predicate = null);
        T Get(Expression<Func<T, bool>> predicate);
        void Adicionar(T entity);
        void Atualizar(T entity);
        void Deletar(T entity);
        int Contar();
    }

Na implementação do nosso repositório genérico, temos um tipo abstrato (interface) contendo operações atômicas básicas que serão usadas em nossa aplicação para interagir com todos os repositórios.

A seguir, temos a implementação da interface IRepositorio<T> pela classe concreta Repositorio<T>:

public class Repositorio<T> : IRepositorio<T> where T : class
    {
        private AppContexto m_Context = null;
        DbSet<T> m_DbSet;
        public Repositorio(AppContexto context)
        {
            m_Context = context;
            m_DbSet = m_Context.Set<T>();
        }
        public IEnumerable<T> GetTudo(Expression<Func<T, bool>> predicate = null)
        {
            if (predicate != null)
            {
                return m_DbSet.Where(predicate);
            }
            return m_DbSet.AsEnumerable();
        }
        public T Get(Expression<Func<T, bool>> predicate)
        {
            return m_DbSet.FirstOrDefault(predicate);
        }
        public void Adicionar(T entity)
        {
            m_DbSet.Add(entity);
        }
        public void Atualizar(T entity)
        {
            m_DbSet.Attach(entity);
            ((IObjectContextAdapter)m_Context).ObjectContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
        }
        public void Deletar(T entity)
        {
            m_DbSet.Remove(entity);
        }
        public int Contar()
        {
            return m_DbSet.Count();
        }
    }

A implementação da interface IRepostiorio<T> é feita pela classe concreta Repositorio<T>, que delega todas as chamadas para o contexto associado (DBContext).

No construtor solicitamos uma instância do DbContext que será fornecida pela Unit of Work – aqui, para aplicações mais simples, poderíamos usar diretamente o repositório no controlador.

A próxima etapa é implementar a Unit of Work para poder encapsular os repositórios.

Aguarde!