Back-End

19 set, 2018

Advanced Repository Pattern com Entity Framework Core

Publicidade

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.