Dev (Back & Front)ARTIGO

Advanced Repository Pattern com Entity Framework Core

No seguinte artigo, o autor apresenta uma implementação mais avançada do padrão de repositório usando o Entity Framework Core.

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!

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.

Escritor, Palestrante e Arquiteto Back-End na Accenture, uma das maiores consultorias do mundo. Um verdadeiro entusiasta do código, apaixonado por tecnologia e empreendedorismo. Em seu tempo livre gosta de escrever artigos técnicos, compartilhar algumas samples no github e acompanhar as novidades da comunidade .NET. Atualmente suas tecnologias preferidas são: Docker, .Net Core, Node e RabbitMQ. Acredita que programar vai além de copiar código e por isso contribui com artigos e palestras para evolução da comunidade de programadores, afinal programador bom é programador bem conceituado.

Ver perfil