Fala, pessoal!
Hoje venho compartilhar com vocês um pouco da minha experiência com repositórios. Vou apresentar uma implementação um pouco mais avançada do padrão de repositório. Utilizaremos o ORM (Object Relational Mapping) mais famoso da comunidade .NET: Entity Framework, versão Core.
É válido dizer que os exemplos aqui explicitados são para fins acadêmicos. Antes de simplesmente plugar este repositório no seu projeto, aconselho a realizar uma análise para ver se realmente é preciso. Sempre que possível, evite a injeção de código desnecessário somente por elegância.
Coding
Vamos começar pelas restrições da classe RepositoryBase:
public abstract class RepositoryBase<TEntity, TContext> where TEntity : Entity where TContext : DbContext
Perceba que o repositório base deve receber dois parâmetros: TEntity e TContext. Onde TEntity deve ser uma entidade e TContext um contexto (DbContext). A ideia de utilizar um contexto genérico no repositório, é permitir que a aplicação tenha mais de um contexto.
Imagine um simples cenário, onde o banco de dados é dividido em pelo menos dois schemas. O schema dbo, e um schema personalizado admin. Poderíamos refletir essa estrutura na aplicação separando por contexto. Para um outro artigo, também poderíamos ter cada contexto com uma implementação de unit of work!
protected DbSet<TEntity> DbSet; protected TContext DbContext; protected RepositoryBase(TContext dbContext) { DbContext = dbContext; DbSet = DbContext.Set<TEntity>(); }
Acima, temos duas propriedades e o construtor da classe. A propriedade DbSet representa todas as ações tipadas, disponibilizadas pelo Entity Framework Core para a atual entidade (entidade especificada na restrição da classe ao herdá-la).
Na linha de baixo temos a propriedade TContext, que por sua vez representa o contexto atual (contexto especificado na restrição da classe ao herdá-la). As propriedades estão marcadas como Protected para que estejam acessíveis a(s) classe(s) filha(s), caso necessário.
Vamos ao primeiro método: um GetAll paginado.
public virtual async Task<Tuple<IEnumerable<TEntity>, int>> GetAll ( int skip, int take, bool asNoTracking = true ) { var databaseCount = await DbSet.CountAsync().ConfigureAwait(false); if (asNoTracking) return new Tuple<IEnumerable<TEntity>, int> ( await DbSet.AsNoTracking().Skip(skip).Take(take).ToListAsync().ConfigureAwait(false), databaseCount ); return new Tuple<IEnumerable<TEntity>, int> ( await DbSet.Skip(skip).Take(take).ToListAsync().ConfigureAwait(false), databaseCount ); }
Legal, vamos por partes:
1 – Primeiramente a assinatura do método é virtual, possibilitando realizar um override em classes filhas.
O método tem como retorno uma tupla. Seguindo na linhagem teórica de Eric Evans, um repositório de entidade não deve retornar nada além de suas entidades.
2 – Então, ao invés de colocar um objeto de paginação no retorno do método, existe uma tupla, onde os valores devolvidos são: uma lista de entidades e o count de entidades.
3 – Para invocar o método são necessários dois parâmetros mais um terceiro opcional. É preciso passar o skip (quantidade de registros para pular), take (quantidade de registros para retornar) e o boolean asNoTracking.
Pausa! O que significa o termo tracking quando falamos de EF Core?
Significa que o EF Core pluga um evento e fica “ouvindo” as modificações executadas no retorno da consulta. Imagine um cenário qualquer, onde você realiza uma consulta no banco de dados, utilizando EF Core, e depois realiza uma modificação X de alguns destes dados para exibi-los em tela, porém essas modificações são feitas em memória, e não devem chegar ao banco de dados.
Em seguida, continuando o fluxo do cenário, você realiza outras consultas e modificações Y que realmente devem ser salvas no banco de dados; o que vai acontecer é que, quando você chamar o SaveChanges para gravar as alterações Y, se no mesmo escopo e contexto, o EF Core também terá mapeado as modificações X para as efetivar no banco de dados – tudo que não queríamos.
Utilize a tracking feature quando quiser que o EF Core mapeie as modificações feitas no escopo do retorno atual da consulta. Do contrário, não utilize. Não utilizando os dados serão retornados em modo readonly.
4 – Note também a utilização do ConfigureAwait. Caso em um determinado cenário, não ideal, acabe se utilizando a chamada deste método com .Result ao invés de await, não acontecerá um deadlock.
Uma vez entendido tudo explicado acima, os próximos cenários de consulta são similares.
public virtual async Task<Tuple<IEnumerable<TEntity>, int>> GetAll ( int skip, int take, Expression<Func<TEntity, bool>> where, bool asNoTracking = true ) { var databaseCount = await DbSet.CountAsync().ConfigureAwait(false); if (asNoTracking) return new Tuple<IEnumerable<TEntity>, int> ( await DbSet.AsNoTracking().Where(where).Skip(skip).Take(take).ToListAsync() .ConfigureAwait(false), databaseCount ); return new Tuple<IEnumerable<TEntity>, int> ( await DbSet.Where(where).Skip(skip).Take(take).ToListAsync() .ConfigureAwait(false), databaseCount ); }
Neste segundo GetAll temos exatamente o método anterior com um feature a mais, um filtro where. Para realizar um filtro where, basta fornecer um parâmetro de expressão de um função lambda de Entidade onde o resultado é booleano, ou seja, uma determinada condição. Exemplo: todos usuários que estão ativos na base de dados. E se tem where, por que não ter orderBy?
public virtual async Task<Tuple<IEnumerable<TEntity>, int>> GetAll ( int skip, int take, Expression<Func<TEntity, bool>> where, Expression<Func<TEntity, object>> orderBy, bool asNoTracking = true ) { var databaseCount = await DbSet.CountAsync().ConfigureAwait(false); if (asNoTracking) return new Tuple<IEnumerable<TEntity>, int> ( await DbSet.AsNoTracking().OrderBy(orderBy).Where(where).Skip(skip).Take(take).ToListAsync() .ConfigureAwait(false), databaseCount ); return new Tuple<IEnumerable<TEntity>, int> ( await DbSet.OrderBy(orderBy).Where(where).Skip(skip).Take(take).ToListAsync() .ConfigureAwait(false), databaseCount ); }
No terceiro GetAll utilizaremos um orderBy! O Parâmetro é idêntico ao where e a única diferença é a troca do boolean da função por um object, significando que a consulta pode ser ordenada por uma coluna de qualquer tipo!
Mais à frente passamos de métodos de leitura para métodos de escrita.
public virtual async Task AddAsync(TEntity entity) { await DbSet.AddAsync(entity).ConfigureAwait(false); } public virtual async Task AddCollectionAsync(IEnumerable<TEntity> entities) { await DbSet.AddRangeAsync(entities).ConfigureAwait(false); } public virtual IEnumerable<TEntity> AddCollectionWithProxy(IEnumerable<TEntity> entities) { foreach (var entity in entities) { DbSet.Add(entity); yield return entity; } }
Acima temos algumas implementações já conhecidas, como: AddAsync e AddCollectionAsync, como vocês já conhecem. Vamos discorrer sobre o método mais diferente, o AddCollectionWithProxy. Este método, recebe uma coleção de entidades, as itera adicionando no banco de dados e então retorna uma a uma utilizando a palavra chave yield. Isso significa que o retorno para a camada que chamou o método AddCollectionWithProxy será feito a cada iteração.
Essa implementação é muito útil quando existe uma camada de validação antes da de repositório, onde ao invés de validar, cem (número arbitrário) objetos de uma vez só, você precisa validar um e então inserir, e em caso de falha de algum objeto, todos próximos precisam ser cancelados.
Por exemplo, nesta mesma circunstância de uma coleção de cem (número arbitrário) objetos, o fluxo ficaria da seguinte maneira: valida o primeiro objeto na camada de validação, sucesso, insere o objeto (camada de repositório), volta para camada de validação, valida o segundo objeto, sucesso, insere o objeto (camada de repositório), volta para camada de validação, valida o terceiro objeto, falha, interrompe a inserção deste e dos próximos noventa e seis objetos. Muito útil em cenários complexos!
Bem da hora, né? Mesmo cenário abaixo para o método de update!
public virtual Task UpdateAsync(TEntity entity) { DbSet.Update(entity); return Task.CompletedTask; } public virtual Task UpdateCollectionAsync(IEnumerable<TEntity> entities) { DbSet.UpdateRange(entities); return Task.CompletedTask; } public virtual IEnumerable<TEntity> UpdateCollectionWithProxy(IEnumerable<TEntity> entities) { foreach (var entity in entities) { DbSet.Update(entity); yield return entity; } }
Para fechar o CRUD (Create, Read, Update, Delete). Um método de Remove Range passando uma função lambda, onde serão removidos todos que corresponderem àquela condição.
public virtual Task RemoveByAsync(Func<TEntity, bool> where) { DbSet.RemoveRange(DbSet.ToList().Where(where)); return Task.CompletedTask; } public virtual Task RemoveAsync(TEntity entity) { DbSet.Remove(entity); return Task.CompletedTask; }
Para fechar temos o SaveChanges, também de forma independente.
public virtual async Task SaveChangesAsync() { await DbContext.SaveChangesAsync().ConfigureAwait(false); }
Observação 1: note que alguns métodos não estão marcados com a palavra reservada async na assinatura, mas mesmo assim coloquei o sufixo Async na nomenclatura, isso porque o que define se o método executará de forma assíncrona ou não é sua chamada, e não sua implementação.
Observação 2: em alguns pontos, como nos GetAlls, seria possível realizar a chamada de um método dentro de outro. Exemplo: dentro do GetAll que tem where e orderBy, poderia ser realizada a chamada do GetAll que já tem o where e seria apenas acrescentado o orderBy.
Porém, não se trata de uma boa prática. Interligando os métodos seria a mesma coisa que acoplá-los. E se em um momento fosse necessário realizar uma refatoração, onde precisaria de apenas um refactory passaria a precisar de dois, três.
Extra
Outra coisa legal, é: EF Core ainda não tinha a feature de LazyLoading até a versão 2.1 do .Net Core, como já estamos na 2.2. Já podemos utilizar novamente o carregamento sob demanda. Para isso, basta rodar:
Install-Package Microsoft.EntityFrameworkCore.Proxies
Para uma maneira de uso mais simplificada, basta habilitar a utilização do proxy de lazy loading no override do metodo OnConfiguring.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(_connectionString);
Pronto, agora você já pode utilizar da feature de lazy loading no Entity Framework Core! Não se esqueça de configurar suas propriedades de navegação como virtuais!
É isso, pessoal! Agradeço a leitura. Um grande abraço e até a próxima!
- Código completo: https://github.com/kenerry-serain/AdvancedRepositoryPattern
Quer saber mais sobre docker, docker compose, .net core, testes unitários e de integração? Baixe meu e-book free em: https://kenerry.com.br.