10 01 2018
Desacoplando o acesso a dados da aplicação com o padrão Repository e Unit of Work
Os padrões Repository e Unit of Work são design patterns muito conhecidos e a cada dia mais utilizados no desenvolvimento de aplicações de negócios. O grande problema é que a maioria dos exemplos que encontramos desses padrões é voltada para desenvolvimento web, ou utiliza uma tecnologia muito específica na implementação (por exemplo, Repository para Entity Framework). O objetivo do artigo de hoje é explicar os conceitos do padrão Repository e Unit of Work, listar as vantagens e desvantagens da sua utilização, bem como implementar um exemplo desses padrões em uma Console Application.
Disclaimer
Eu sempre gosto de deixar bem claro nos meus artigos e vídeos quando eu estou falando de algo que eu não utilizo no dia a dia. Se você já consumiu algum conteúdo meu que não tenha uma seção de “disclaimer“, isso significa que eu já coloquei em prática o assunto que foi abordado e que eu relativamente domino aquela área. Esse não é o caso do padrão Repository e Unit of Work, uma vez que eu nunca os utilizei em produção.
Até o momento em que eu parei para escrever esse artigo, eu nunca tinha nem parado para implementar um exemplo utilizando esses padrões! Eu já sabia exatamente o conceito por trás deles e as suas vantagens. Além disso, eu já tinha consumido inúmeros conteúdos de outras pessoas onde esses padrões tinham sido utilizados.
Dito isso, tudo o que eu vou mostrar no artigo de hoje foi o resultado do meu aprendizado nos últimos dias ao preparar um exemplo de Console Application utilizando os padrões Repository e Unit of Work. Essa Console Application será totalmente desacoplada da camada de acesso a dados concreta, possibilitando o armazenamento de dados tanto em memória quanto em um banco de dados (utilizando Entity Framework Code First).
Se você quiser conferir outros conteúdos de pessoas que aparentemente já têm mais experiência com esses padrões, eu recomendo que você dê uma olhada nos seguintes links (que me ajudaram como base na construção deste artigo):
– Creating a Data Repository using Dapper (indicação do leitor Fausto Luis)
– Ebook do Fabio Silva Lima sobre Design Patterns vol.1
– Repository Pattern with C# and Entity Framework, Done Right
– Quando usar Entity Framework com Repository Pattern?
– Entity Framework Repositório Genérico
O que é o padrão Repository?
Como mencionei anteriormente, o padrão Repository é um design pattern que pode ajudar muito quando implementamos o acesso a dados das nossas aplicações. Ele abstrai toda a parte de armazenamento de dados e consultas ao banco, servindo como um mediador entre a aplicação e a camada de mapeamento de objetos.
Quando utilizamos o padrão Repository, todas as nossas consultas customizadas são implementadas em um único local, evitando a repetição de código. Por exemplo, imagine que em uma determinada aplicação nós precisemos retornar todos os produtos de uma determinada categoria. Onde é que nós implementaríamos esse tipo de código? Direto na aplicação? E se nós precisarmos utilizar essa mesma consulta mais de uma vez? Como fazemos para não repetirmos o mesmo código em locais diferentes da aplicação?
Uma alternativa seria trabalharmos com uma camada de Data Access Objects, onde implementaríamos tanto os códigos do CRUD (criação, alteração, exclusão e listagem) quando as consultas customizadas. Outra opção seria trabalharmos com o padrão Repository, que é justamente o que eu vou mostrar para você no artigo de hoje.
O padrão Repository se resume basicamente a uma interface genérica, chamada de IRepository. Essa interface definirá o contrato dos métodos básicos de inclusão (Add), remoção (Remove) e listagem de registros (GetAll) que todo repositório concreto deverá implementar:
Nota: os nomes para os métodos podem variar dependendo da implementação. Algumas pessoas optam por “Create” ao invés de “Add” e “Delete” ao invés de “Remove”. Eu escolhi esses nomes para manter o mesmo padrão das implementações de List (do .NET) e DbSet (do Entity Framework).
Uma vez estabelecido esse padrão, para cada entidade da nossa aplicação, nós teremos que criar um repositório concreto. Ou seja, se tivermos uma entidade “Produto“, nós teremos que ter um repositório de produtos (ProdutoRepository). Se tivermos uma entidade “Cliente“, precisaremos de um repositório para ele também (ClienteRepository), e assim por diante.
Dentro dos repositórios específicos, além dos métodos básicos de inclusão, remoção e listagem de registros, nós teremos os métodos de consultas customizadas. No exemplo que dei anteriormente do método que retornaria todos os produtos de uma determinada categoria, esse método seria definido na interface de repositório específica do produto (IProdutoRepository).
E o Unit of Work?
Note que na explicação acima sobre o padrão Repository, eu não falei nada sobre a parte de “salvar” as entidades. A responsabilidade do repositório é simplesmente armazenar uma coleção de entidades em memória. A persistência das informações não é de responsabilidade do repositório, mesmo porque, como conseguiríamos controlar transações feitas em mais de um repositório se a responsabilidade da persistência estivesse nesse elemento? Por exemplo, como faríamos para transacionar a operação de venda e baixa de estoque se a responsabilidade de salvar as informações estivesse separada nos repositórios de venda e estoque?
Para resolvermos esse problema, temos outro design pattern chamado Unit of Work. Esse padrão se resume a uma interface (IUnitOfWork) que terá todos os repositórios da aplicação, além de um método que será responsável por salvar as alterações (Save).
Na aplicação que iremos construir neste artigo, nós teremos dois repositórios. Um repositório conterá os produtos e outro repositório conterá as categorias de produtos. Dessa forma, a interface IUnitOfWork nesse projeto de exemplo terá os dois repositórios e o método “Save“:
Não se preocupe se as coisas não estiverem claras até aqui. Eu também demorei um pouco para entender completamente esses dois padrões. Daqui a pouco nós veremos um exemplo concreto e tudo ficará mais claro.
Vantagens ao utilizar esses padrões
A primeira vantagem que temos ao utilizar os padrões Repository e Unit of Work é que, como mencionei anteriormente, as consultas complexas ficam todas armazenadas em um único local. Além disso, diferentemente dos DAOs, com o padrão Repository fica mais fácil de implementarmos um controle de acesso misto (onde, por exemplo, consultas seriam feitas com Dapper e o resto ficaria a cargo de algum ORM mais robusto).
Além disso, ao implementarmos esses padrões, nós podemos facilmente substituir de forma completa a camada de persistência de dados da nossa aplicação. Ou seja, nós poderíamos trocar o armazenamento de dados de um ORM para outro (do Entity Framework para o NHibernate, por exemplo) sem que o código central da aplicação precise ser modificado. Resumindo, a aplicação não tem a mínima ideia de onde os dados estão sendo armazenados, ela só conhece os repositórios genéricos.
Por fim, outra grande vantagem que temos ao utilizarmos repositórios é na parte de “testabilidade“. Com o padrão Repository e Unit of Work, fica muito mais fácil criarmos “mocks” para testarmos as regras de negócio da nossa aplicação de forma independente do banco de dados.
Quando não devemos utiliza-los?
Esses padrões, apesar de serem muito úteis, trazem uma certa complexidade adicional ao nosso código. Se o domínio da sua aplicação não for complexo, não faz sentido adicionar toda essa parafernália.
Além disso, se você tiver certeza absoluta que você não precisará trocar a camada de persistência da sua aplicação, você pode continuar com os seus DAOs mesmo, uma vez que, com eles, você conseguirá atingir basicamente o mesmo benefício desses padrões.
Padrão Repository e Unit of Work genéricos na prática
Eu sei que tudo o que eu falei até agora está muito abstrato, mas acho que a partir desse momento as coisas começarão a ficar mais claras. Vamos implementar na prática o padrão Repository e Unit of Work em uma aplicação de exemplo que conterá duas entidades: Produtos e Categorias de Produtos. Tudo isso será feito em uma aplicação console e, como resultado final nós conseguiremos, com uma linha de código, trocar completamente a estratégia de armazenamento de dados da aplicação (em memória / no banco de dados em Entity Framework).
Para não nos confundirmos, nós iremos separar a implementação em projetos diferentes. Um projeto será a Console Application e, inicialmente, teremos um outro projeto que conterá as entidades e repositórios genéricos.
Então vamos começar criando um projeto do tipo Console Application e, na mesma solução, vamos adicionar um novo projeto do tipo “Class Library“. Dentro desse projeto “Class Library“, nós vamos adicionar as nossas duas entidades (CategoriaProduto e Produto):
// C# public class CategoriaProduto { public int Id { get; set; } public string Nome { get; set; } }
// C# public class Produto { public int Id { get; set; } public string Descricao { get; set; } public CategoriaProduto Categoria { get; set; } public int CategoriaId { get; set; } public DateTime DataCadastro { get; set; } public decimal ValorUnitario { get; set; } }
' VB.NET Public Class CategoriaProduto Public Property Id As Integer Public Property Nome As String End Class
' VB.NET Public Class Produto Public Property Id As Integer Public Property Descricao As String Public Property Categoria As CategoriaProduto Public Property CategoriaId As Integer Public Property DataCadastro As DateTime Public Property ValorUnitario As Decimal End Class
Em seguida, vamos adicionar uma nova pasta (chamada “Repository“) no projeto “Class Library“, onde colocaremos os repositórios genéricos da nossa aplicação. Dentro dessa pasta, vamos adicionar a nossa interface “IRepository“, que terá a estrutura que mencionei anteriormente, com os métodos “GetAll“, “Add” e “Remove“:
// C# public interface IRepository<T> where T : class { IEnumerable<T> GetAll(); void Add(T entity); void Remove(T entity); }
' VB.NET Namespace Repository Public Interface IRepository(Of T As Class) Function GetAll() As IEnumerable(Of T) Sub Add(ByVal entity As T) Sub Remove(ByVal entity As T) End Interface End Namespace
Nota: se você não sabe o que é esse “T” na definição da interface, ele está basicamente indicando que a interface é genérica, ou seja, ela é ligada a algum tipo. Nesse caso, os repositórios serão de um tipo específico (qualquer classe, como definido na cláusula “where”). No nosso caso, nós teremos repositório de produtos e categorias de produtos, como veremos logo a seguir. É a mesma ideia de listas genérica de algum tipo (“List<string>”, por exemplo). Para mais informações sobre esse assunto, pesquise por “generics” no .NET.
Agora que já temos a nossa interface de repositório genérica, chegou a hora de definirmos as interfaces de repositório da nossa aplicação. No nosso caso, nós teremos uma interface de repositório para as categorias de produtos (ICategoriaProdutoRepository) e outra interface de repositório para os produtos (IProdutoRepository).
Na interface de repositório para as categorias de droduto, nós não teremos nenhum código customizado. Porém, na interface de repositório para os produtos, nós definiremos o contrato para o método que deverá retornar os produtos de uma determinada categoria (GetProdutosPorCategoria):
// C# public interface ICategoriaProdutoRepository : IRepository<CategoriaProduto> { }
// C# public interface IProdutoRepository : IRepository<Produto> { IEnumerable<Produto> GetProdutosPorCategoria(CategoriaProduto categoria); }
' VB.NET Namespace Repository Public Interface ICategoriaProdutoRepository Inherits IRepository(Of CategoriaProduto) End Interface End Namespace
' VB.NET Namespace Repository Public Interface IProdutoRepository Inherits IRepository(Of Produto) Function GetProdutosPorCategoria(ByVal categoria As CategoriaProduto) As IEnumerable(Of Produto) End Interface End Namespace
Por fim, só está faltando a definição do nosso Unit of Work. No nosso caso, dentro da nossa interface IUnitOfWork nós teremos uma instância de cada interface de repositório, além do método “Save“:
// C# public interface IUnitOfWork { ICategoriaProdutoRepository CategoriaProduto { get; } IProdutoRepository Produto { get; } void Save(); }
' VB.NET Namespace Repository Public Interface IUnitOfWork ReadOnly Property CategoriaProduto As ICategoriaProdutoRepository ReadOnly Property Produto As IProdutoRepository Sub Save() End Interface End Namespace
E com isso nós finalizamos a implementação do nosso Repository e Unit of Work genéricos. Agora nós podemos desenvolver uma aplicação que só conhecerá essas interfaces, de forma que ela não tenha nenhuma dependência com uma implementação concreta. Vamos fazer isso no nosso projeto Console Application, que deverá ter uma referência para o projeto “Class Library“, onde implementamos as entidades e o repositório genérico.
Dentro da classe “Program” nós definiremos uma instância da nossa interface “IUnitOfWork“. Em seguida, dentro do método “main“, nós faremos um workflow de listagem, criação, alteração e exclusão de produtos:
// C# private static Model.Repository.IUnitOfWork contexto; static void Main(string[] args) { contexto = null; List(); Create(); List(); Update(); List(); Delete(); List(); System.Console.ReadLine(); }
' VB.NET Private Contexto As Model.VB.Repository.IUnitOfWork Public Sub Main() Contexto = Nothing List() Create() List() Update() List() Delete() List() System.Console.ReadLine() End Sub
Nota: por enquanto nós setamos o valor do nosso contexto para “nulo”, uma vez que nós ainda não temos nenhuma implementação concreta do repositório. Obviamente esse código não funcionará até o momento em que nós substituirmos essa declaração por uma implementação concreta do repositório.
Veja só o código dos métodos “List“, “Create“, “Update” e “Delete“:
// C# private static void List() { System.Console.WriteLine("======= LISTAGEM DE PRODUTOS ======="); foreach (var produto in contexto.Produto.GetAll()) { System.Console.WriteLine("Id: {0}, Descricao: {1}, Categoria: {2}, Data Cadastro: {3:d}, Valor Unitário: {4:n2}", produto.Id, produto.Descricao, produto.Categoria.Nome, produto.DataCadastro, produto.ValorUnitario); } System.Console.WriteLine("===================================="); } private static void Create() { var novoProduto = new Model.Produto(); novoProduto.Descricao = "Novo produto"; novoProduto.Categoria = contexto.CategoriaProduto.GetAll().First(); novoProduto.DataCadastro = DateTime.Now; novoProduto.ValorUnitario = 2.34m; contexto.Produto.Add(novoProduto); contexto.Save(); System.Console.WriteLine("===================================="); System.Console.WriteLine("PRODUTO CADASTRADO COM SUCESSO"); System.Console.WriteLine("===================================="); } private static void Update() { var produto = contexto.Produto.GetAll().Last(); produto.Descricao = "Produto alterado"; contexto.Save(); System.Console.WriteLine("===================================="); System.Console.WriteLine("PRODUTO ALTERADO COM SUCESSO"); System.Console.WriteLine("===================================="); } private static void Delete() { var produto = contexto.Produto.GetAll().Last(); contexto.Produto.Remove(produto); contexto.Save(); System.Console.WriteLine("===================================="); System.Console.WriteLine("PRODUTO EXCLUÍDO COM SUCESSO"); System.Console.WriteLine("===================================="); }
' VB.NET Private Sub List() System.Console.WriteLine("======= LISTAGEM DE PRODUTOS =======") For Each Produto In Contexto.Produto.GetAll() System.Console.WriteLine("Id: {0}, Descricao: {1}, Categoria: {2}, Data Cadastro: {3:d}, Valor Unitário: {4:n2}", Produto.Id, Produto.Descricao, Produto.Categoria.Nome, Produto.DataCadastro, Produto.ValorUnitario) Next System.Console.WriteLine("====================================") End Sub Private Sub Create() Dim NovoProduto = New Model.VB.Produto() NovoProduto.Descricao = "Novo produto" NovoProduto.Categoria = Contexto.CategoriaProduto.GetAll().First() NovoProduto.DataCadastro = DateTime.Now NovoProduto.ValorUnitario = 2.34D Contexto.Produto.Add(NovoProduto) Contexto.Save() System.Console.WriteLine("====================================") System.Console.WriteLine("PRODUTO CADASTRADO COM SUCESSO") System.Console.WriteLine("====================================") End Sub Private Sub Update() Dim Produto = Contexto.Produto.GetAll().Last() Produto.Descricao = "Produto alterado" Contexto.Save() System.Console.WriteLine("====================================") System.Console.WriteLine("PRODUTO ALTERADO COM SUCESSO") System.Console.WriteLine("====================================") End Sub Private Sub Delete() Dim Produto = Contexto.Produto.GetAll().Last() Contexto.Produto.Remove(Produto) Contexto.Save() System.Console.WriteLine("====================================") System.Console.WriteLine("PRODUTO EXCLUÍDO COM SUCESSO") System.Console.WriteLine("====================================") End Sub
Observe que a aplicação não tem a mínima ideia de onde os dados serão armazenados. Ela só conhece as interfaces genéricas e não sabe de nenhuma implementação concreta do repositório.
Repository em memória
Agora que nós já temos a implementação da nossa aplicação, chegou a hora de criarmos um repositório concreto. O primeiro repositório concreto que criaremos armazenará os dados em memória, dentro de listas genéricas. Para não misturarmos as coisas, vamos criar um novo projeto “Class Library” onde faremos essa implementação. Esse novo projeto deverá referenciar o outro projeto onde as entidades foram definidas.
Dentro desse projeto, vamos adicionar uma classe chamada “Repository“. Essa será a classe que servirá de base para todos os outros repositórios concretos. Vamos ver como é que fica o código dessa classe:
// C# public abstract class Repository<T> : Model.Repository.IRepository<T> where T : class { protected List<T> _lista = new List<T>(); public virtual void Add(T entity) { _lista.Add(entity); } public virtual IEnumerable<T> GetAll() { return _lista.AsEnumerable(); } public virtual void Remove(T entity) { _lista.Remove(entity); } }
' VB.NET Public MustInherit Class Repository(Of T As Class) Implements Model.VB.Repository.IRepository(Of T) Protected Lista As List(Of T) = New List(Of T) Public Overridable Sub Add(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Add Me.Lista.Add(Entity) End Sub Public Overridable Function GetAll() As IEnumerable(Of T) Implements Model.VB.Repository.IRepository(Of T).GetAll Return Me.Lista.AsEnumerable End Function Public Overridable Sub Remove(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Remove Me.Lista.Remove(Entity) End Sub End Class
Observe que essa classe implementa a nossa interface genérica (IRepository), utilizando uma lista por trás dos panos, onde os dados serão armazenados.
Em seguida, vamos adicionar o nosso repositório concreto para as categorias dos produtos. Esse repositório não terá nenhum código customizado, ele será somente uma implementação concreta de “Repository<CategoriaProduto>“:
// C# public class CategoriaProdutoRepository : Repository<Model.CategoriaProduto>, Model.Repository.ICategoriaProdutoRepository { }
' VB.NET Public Class CategoriaProdutoRepository Inherits Repository(Of Model.VB.CategoriaProduto) Implements Model.VB.Repository.ICategoriaProdutoRepository End Class
Por outro lado, o repositório concreto para os produtos deverá implementar duas lógicas adicionais. Uma delas é o método “GetProdutosPorCategoria“, que foi definido no contrato da interface “IProdutoRepository“. O outro código customizado é o cálculo do auto-incremento para o “Id” do Produto. Como nós não estamos salvando essas informações no banco de dados, nós temos que calcular o auto-incremento manualmente:
// C# public class ProdutoRepository : Repository<Model.Produto>, Model.Repository.IProdutoRepository { public override void Add(Model.Produto entity) { if (entity.Id == 0) { entity.Id = GetMaxId() + 1; } base.Add(entity); } public IEnumerable<Model.Produto> GetProdutosPorCategoria(Model.CategoriaProduto categoria) { return _lista.Where(p => p.Categoria == categoria).AsEnumerable(); } private int GetMaxId() { var maxId = 0; if (_lista.Any()) { maxId = _lista.Max(p => p.Id); } return maxId; } }
' VB.NET Public Class ProdutoRepository Inherits Repository(Of Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository Public Overrides Sub Add(ByVal Entity As Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository.Add If (Entity.Id = 0) Then Entity.Id = (Me.GetMaxId + 1) End If MyBase.Add(Entity) End Sub Public Function GetProdutosPorCategoria(ByVal Categoria As Model.VB.CategoriaProduto) As IEnumerable(Of Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository.GetProdutosPorCategoria Return Lista.Where(Function(p) p.Categoria Is Categoria).AsEnumerable End Function Private Function GetMaxId() As Integer Dim MaxId = 0 If Lista.Any Then MaxId = Lista.Max(Function(p) p.Id) End If Return MaxId End Function End Class
Por fim, a única coisa que está faltando é implementarmos a interface IUnitOfWork. Para isso, vamos adicionar uma nova classe no projeto, dando o nome de “UnitOfWork“. Essa classe herdará da interface “IUnitOfWork” e terá instâncias dos repositórios concretos que acabamos de criar. Além disso, no construtor nós adicionaremos três categorias padrão na tabela de categorias de produtos:
// C# public class UnitOfWork : Model.Repository.IUnitOfWork { private CategoriaProdutoRepository _categoriaProduto = new CategoriaProdutoRepository(); public Model.Repository.ICategoriaProdutoRepository CategoriaProduto { get { return _categoriaProduto; } } private ProdutoRepository _produto = new ProdutoRepository(); public Model.Repository.IProdutoRepository Produto { get { return _produto; } } public UnitOfWork() { _categoriaProduto.Add(new Model.CategoriaProduto() { Id = 1, Nome = "Categoria 1" }); _categoriaProduto.Add(new Model.CategoriaProduto() { Id = 2, Nome = "Categoria 2" }); _categoriaProduto.Add(new Model.CategoriaProduto() { Id = 3, Nome = "Categoria 3" }); } public void Save() { } }
' VB.NET Public Class UnitOfWork Implements Model.VB.Repository.IUnitOfWork Private _categoriaProduto As CategoriaProdutoRepository = New CategoriaProdutoRepository Public ReadOnly Property CategoriaProduto As Model.VB.Repository.ICategoriaProdutoRepository Implements Model.VB.Repository.IUnitOfWork.CategoriaProduto Get Return Me._categoriaProduto End Get End Property Private _produto As ProdutoRepository = New ProdutoRepository Public ReadOnly Property Produto As Model.VB.Repository.IProdutoRepository Implements Model.VB.Repository.IUnitOfWork.Produto Get Return Me._produto End Get End Property Public Sub New() MyBase.New Me._categoriaProduto.Add(New Model.VB.CategoriaProduto) Me._categoriaProduto.Add(New Model.VB.CategoriaProduto) Me._categoriaProduto.Add(New Model.VB.CategoriaProduto) End Sub Public Sub Save() Implements Model.VB.Repository.IUnitOfWork.Save End Sub End Class
Nota: o método “Save” do UnitOfWork em memória não fará nada, uma vez que os dados não serão persistidos em lugar nenhum.
Agora que já temos a implementação concreta do repositório em memória, vamos voltar ao nosso projeto Console Application, onde faremos uso dessa implementação. Primeiramente, temos que adicionar uma referência à nova “Class Library” onde a implementação do repositório em memória foi realizada. Em seguida, basta alterarmos a declaração do “IUnitOfWork” para que a nossa classe “UnitOfWork” seja utilizada:
// C# contexto = new InMemory.UnitOfWork();
' VB.NET Contexto = New InMemory.VB.UnitOfWork()
Execute a aplicação e veja o resultado:
Repository com Entity Framework
OK, a nossa aplicação está funcionando perfeitamente com o armazenamento dos dados em memória. Mas, e se agora nós quisermos armazenar as informações em um banco utilizando Entity Framework? É aí que nós começaremos a enxergar as vantagens do padrão Repository e Unit of Work. Basta nós implementarmos o repositório com Entity Framework, trocar a declaração para que o novo UnitOfWork concreto seja utilizado e pronto! A aplicação nem saberá que os dados estão sendo armazenados no banco ao invés de em memória.
Vamos começar criando um novo projeto do tipo “Class Library” e adicionando a referência para o Entity Framework através do NuGet. Se ainda não conhece nada sobre o Entity Framework, dê uma olhada primeiro no meu artigo introdutório sobre o Entity Framework. Ele é leitura obrigatória se você ainda não conhece os conceitos básicos do Entity Framework.
Uma vez criado o projeto e adicionada a referência para o Entity Framework, vamos adicionar uma nova classe chamada “Contexto“, que será o nosso contexto do Entity Framework:
// C# internal class Contexto : System.Data.Entity.DbContext { public System.Data.Entity.DbSet<Model.CategoriaProduto> CategoriaProduto { get; set; } public System.Data.Entity.DbSet<Model.Produto> Produto { get; set; } public Contexto() : base() { if (!CategoriaProduto.Any()) { CategoriaProduto.Add(new Model.CategoriaProduto() { Id = 1, Nome = "Categoria 1" }); CategoriaProduto.Add(new Model.CategoriaProduto() { Id = 2, Nome = "Categoria 2" }); CategoriaProduto.Add(new Model.CategoriaProduto() { Id = 3, Nome = "Categoria 3" }); SaveChanges(); } } protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Conventions.Remove<System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention>(); } }
' VB.NET Class Contexto Inherits System.Data.Entity.DbContext Public Property CategoriaProduto As System.Data.Entity.DbSet(Of Model.VB.CategoriaProduto) Public Property Produto As System.Data.Entity.DbSet(Of Model.VB.Produto) Public Sub New() MyBase.New If Not Me.CategoriaProduto.Any Then Me.CategoriaProduto.Add(New Model.VB.CategoriaProduto) Me.CategoriaProduto.Add(New Model.VB.CategoriaProduto) Me.CategoriaProduto.Add(New Model.VB.CategoriaProduto) SaveChanges() End If End Sub Protected Overrides Sub OnModelCreating(ByVal ModelBuilder As System.Data.Entity.DbModelBuilder) MyBase.OnModelCreating(ModelBuilder) ModelBuilder.Conventions.Remove(Of System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention)() End Sub End Class
Como você pode observar, a única coisa que estamos fazendo de especial no contexto é a criação das categorias padrão (caso a tabela de categorias esteja vazia) e a desativação da pluralização das tabelas.
Com o contexto em mãos, chegou a hora de criarmos os nossos repositórios. Primeiramente, vamos adicionar uma classe chamada “Repository“, que servirá como base para todos os repositórios concretos. Veja como fica a implementação dessa classe:
// C# public abstract class Repository<T> : Model.Repository.IRepository<T> where T : class { protected System.Data.Entity.DbSet<T> _dbSet; public Repository(System.Data.Entity.DbSet<T> dbSet) { _dbSet = dbSet; } public virtual void Add(T entity) { _dbSet.Add(entity); } public virtual IEnumerable<T> GetAll() { return _dbSet.AsEnumerable(); } public virtual void Remove(T entity) { _dbSet.Remove(entity); } }
' VB.NET Public MustInherit Class Repository(Of T As Class) Implements Model.VB.Repository.IRepository(Of T) Protected DbSet As System.Data.Entity.DbSet(Of T) Public Sub New(ByVal dbSet As System.Data.Entity.DbSet(Of T)) MyBase.New Me.DbSet = dbSet End Sub Public Overridable Sub Add(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Add Me.DbSet.Add(Entity) End Sub Public Overridable Function GetAll() As IEnumerable(Of T) Implements Model.VB.Repository.IRepository(Of T).GetAll Return Me.DbSet.AsEnumerable End Function Public Overridable Sub Remove(ByVal Entity As T) Implements Model.VB.Repository.IRepository(Of T).Remove Me.DbSet.Remove(Entity) End Sub End Class
Note que a implementação desse repositório com o Entity Framework é muito parecida com a implementação em memória. A principal diferença é que nós estamos trabalhando por trás dos panos com um DbSet do Entity Framework (ao invés de um List em memória). O DbSet deverá ser passado por parâmetro no construtor.
Em seguida, vamos partir para a implementação dos repositórios CategoriaProdutoRepository e ProdutoRepository:
// C# public class CategoriaProdutoRepository : Repository<Model.CategoriaProduto>, Model.Repository.ICategoriaProdutoRepository { public CategoriaProdutoRepository(System.Data.Entity.DbSet<Model.CategoriaProduto> dbSet) : base(dbSet) { } }
// C# public class ProdutoRepository : Repository<Model.Produto>, Model.Repository.IProdutoRepository { public ProdutoRepository(System.Data.Entity.DbSet<Model.Produto> dbSet) : base(dbSet) { } public IEnumerable<Model.Produto> GetProdutosPorCategoria(Model.CategoriaProduto categoria) { return _dbSet.Where(p => p.Categoria == categoria).AsEnumerable(); } }
' VB.NET Public Class CategoriaProdutoRepository Inherits Repository(Of Model.VB.CategoriaProduto) Implements Model.VB.Repository.ICategoriaProdutoRepository Public Sub New(ByVal DbSet As System.Data.Entity.DbSet(Of Model.VB.CategoriaProduto)) MyBase.New(DbSet) End Sub End Class
' VB.NET Public Class ProdutoRepository Inherits Repository(Of Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository Public Sub New(ByVal DbSet As System.Data.Entity.DbSet(Of Model.VB.Produto)) MyBase.New(DbSet) End Sub Public Function GetProdutosPorCategoria(ByVal Categoria As Model.VB.CategoriaProduto) As IEnumerable(Of Model.VB.Produto) Implements Model.VB.Repository.IProdutoRepository.GetProdutosPorCategoria Return DbSet.Where(Function(p) p.Categoria Is Categoria).AsEnumerable End Function End Class
Observe que dessa vez a implementação do repositório de produtos não precisa se preocupar com o cálculo do próximo “Id” do produto (como tivemos que fazer com o repositório em memória), uma vez que isso fica a cargo do Entity Framework.
Por fim, só está faltando a implementação da classe UnitOfWork. Veja como é que fica o código dela:
// C# public class UnitOfWork : Model.Repository.IUnitOfWork { private Contexto _contexto; private Model.Repository.ICategoriaProdutoRepository _categoriaProduto; public Model.Repository.ICategoriaProdutoRepository CategoriaProduto { get { return _categoriaProduto; } } private Model.Repository.IProdutoRepository _produto; public Model.Repository.IProdutoRepository Produto { get { return _produto; } } public UnitOfWork() { _contexto = new Contexto(); _categoriaProduto = new CategoriaProdutoRepository(_contexto.CategoriaProduto); _produto = new ProdutoRepository(_contexto.Produto); } public void Save() { _contexto.SaveChanges(); } }
' VB.NET Public Class UnitOfWork Implements Model.VB.Repository.IUnitOfWork Private Contexto As Contexto Private _categoriaProduto As Model.VB.Repository.ICategoriaProdutoRepository Public ReadOnly Property CategoriaProduto As Model.VB.Repository.ICategoriaProdutoRepository Implements Model.VB.Repository.IUnitOfWork.CategoriaProduto Get Return Me._categoriaProduto End Get End Property Private _produto As Model.VB.Repository.IProdutoRepository Public ReadOnly Property Produto As Model.VB.Repository.IProdutoRepository Implements Model.VB.Repository.IUnitOfWork.Produto Get Return Me._produto End Get End Property Public Sub New() MyBase.New Me.Contexto = New Contexto Me._categoriaProduto = New CategoriaProdutoRepository(Me.Contexto.CategoriaProduto) Me._produto = New ProdutoRepository(Me.Contexto.Produto) End Sub Public Sub Save() Implements Model.VB.Repository.IUnitOfWork.Save Me.Contexto.SaveChanges() End Sub End Class
Note que a implementação não é muito complicada. Nós só temos as variáveis e propriedades para cada um dos repositórios, além de uma variável para o contexto do Entity Framework. Os repositórios são instanciados no construtor passando os respectivos DbSets do contexto. Além disso, o método “Save” simplesmente chama um “SaveChanges” no contexto do Entity Framework.
E agora chegou a hora da grande “mágica“. Se nós adicionarmos uma referência no nosso projeto Console Application para essa nova “Class Library” onde o repositório do Entity Framework foi implementado, nós conseguimos trocar completamente a estratégia de armazenamento de dados para a nossa aplicação. Ao invés de armazenarmos em memória, nós armazenaremos no banco de dados utilizando o Entity Framework:
// C# contexto = new EF.UnitOfWork();
' VB.NET Contexto = New EF.VB.UnitOfWork()
Execute a aplicação e veja o resultado:
E o banco de dados foi criado automaticamente por trás dos panos no LocalDb:
Dificuldades do Repository com ADO.NET puro ou Dapper
Será que é possível utilizarmos o padrão Repository e Unit of Work com ADO.NET puro ou Dapper? Apesar de ser possível, nós teremos algumas dificuldades se quisermos seguir esse padrão em que o repositório não é responsável pela persistência dos dados, mas sim o Unit of Work. Nesse caso teríamos que controlar o estado das entidades para vermos se elas foram alteradas, e aí sim fazermos um INSERT, UPDATE ou DELETE no banco de dados dependendo das alterações que foram feitas.
Uma alternativa seria trazer a responsabilidade da atualização dos dados para o repositório (ou seja, nós teríamos um método “Update” nele também), descartando então o Unit of Work. Porém, isso tornaria a implementação meio que incompatível com ORMs como Entity Framework e NHibernate.
Quem sabe em uma próxima oportunidade eu tente explorar essas duas possibilidades, mas hoje nós vamos ficando por aqui.
Baixe o projeto de exemplo
Para baixar o projeto de exemplo desse artigo, assine a minha newsletter. Ao fazer isso, além de ter acesso ao projeto, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário no final do artigo.
Concluindo
O armazenamento de informações é uma parte crucial de qualquer aplicação de negócios. Muitas vezes, nós acabamos implementando a aplicação de maneira completamente acoplada a uma estratégia de persistência de dados (ADO.NET, Entity Framework, NHibernate, Dapper, etc). Aplicações desenvolvidas dessa maneira funcionam muito bem até o momento em que precisamos trocar a camada de armazenamento de dados.
Para desacoplarmos o acesso a dados da aplicação, nós podemos utilizar o padrão Repository e Unit of Work. Outra vantagem que temos ao utilizarmos esses padrões é a concentração de consultas customizadas em um único lugar, evitando repetição de código.
Você já trabalhou com esses design patterns? Como foram as suas experiências com eles? Eu mencionei no início do artigo que eu nunca trabalhei profissionalmente com eles, mas depois de ter escrito esse artigo eu consegui entender a importância deles e muito provavelmente vou acabar utilizando esses padrões em novas aplicações no futuro. Fico aguardando as suas observações nos comentários logo abaixo.
Até a próxima!
André Lima
Revisão de 2017, metas para 2018 Comparando a data/hora com um servidor NTP no .NET
Parabéns pelo conteúdo….seria bom se tivesse prosseguimento no mesmo tema, antes de ir para um próximo….para o aluno não perder o foco!!
Olá Claudio, muito obrigado pelo comentário!
Infelizmente não vou conseguir dar continuidade com esse tema nas próximas semanas, uma vez que eu já tinha outros conteúdos prontos e agendados.. Mas, pode ficar tranquilo que já está nos meus planos a segunda parte desse artigo..
Abraço!
André Lima
Muito Bom. Iniciantes devem ter ficado com “água na boca”…
Em relação a “… teríamos que controlar o estado das entidades para vermos se elas foram alteradas, e aí sim fazermos um INSERT, UPDATE ou DELETE no banco de dados dependendo das alterações que foram feitas.”, penso que o artigo + comentários em “https://www.codeproject.com/Articles/581487/Unit-of-Work-Design-Pattern” poderá ser esclarecedor.
Um ou outro pormenor que poderá querer incluir na sua agenda:
– DI (Direct Injection);
– UoW genérico () para quando temos muitas tabelas/POCO’s para gerir;
– Factory para gerir conexões (através da leitura do app.config, p. exº, podermos escolher com facilidade, qual o data provider que iremos utilizar (com recurso ao IDbConnection, p. exº));
– Implementação no repositório UnitOfWork, da operação Save/Commit/Complete e consequente Rollback.
Abraço
Olá Fausto!
Muito obrigado pelo comentário e pelas sugestões de temas.. Vou colocá-los aqui na minha lista.. :)
Abraço!
André Lima
O InMemory encontramos em qual lib/namespace?
Olá Thamires!
“InMemory” foi só o nome do projeto que eu escolhi para colocar a implementação do repositório que só persiste as informações em memória.. Não é nenhuma referência ou namespace específico do .NET..
Abraço!
André Lima
Ótimo artigo André!
Eu tenho uma dúvida, utilizando Entity Framework seria correto criar um método no repositório que retorne um IQueryable?
Se não, o que seria recomendado caso eu queira fazer Join entre as entidades onde as mesmas não possuem Navigation Properties?
Obrigado!
Olá Anderson!
Vou ser sincero com você.. Eu particularmente não tenho calibre (falta experiência) para conseguir te responder essa sua dúvida.. Mas, eu já vi em muitos lugares que isso (retornar IQueryable em repositório) é uma má prática.. Tem uma discussão sobre esse tema no StackOverflow, talvez te ajude:
Entity Framework Repository Pattern why not return Iqueryable?
Abraço!
André Lima