Cell CMS - Organizando, reutilizando e testando consultas do EntityFramework Core
Tags: csharp, refactor, tutorial, patterns
Published: 2021-02-04 00:37:43

# Cell CMS - Organizando, reutilizando e testando consultas do EntityFramework Core

## Intro

No [último post](https://dev.to/ardc_overflow/cell-cms-criando-testes-de-maneira-pratica-26o9) falamos sobre testes unitários e como tornar a escrita e manutenção deles prática. Mas também tocamos em um assunto controverso para alguns no universo .NET:

> Devo utilizar **EFCore.InMemory** nos meus testes unitários **ou** devo abstrair **uma UnitOfWork e repositórios**?

A ideia do post de hoje é apresentar, brevemente, os dois lados e uma solução que apesar de não ser aderente à Orientação a Objeto é uma solução prática, testável e escalável.

Lembrando que boa parte dos snippets aqui estão implementados no Cell CMS, disponível lá no [GitHub](https://github.com/rodolphocastro/cell-cms/tree/develop)!

{% github rodolphocastro/cell-cms %}

[Cover pega no unDraw.co](https://undraw.co/)

## Motivação para Abstrair o EF Core

A principal motivação para abstrair o EFCore é simples:

> É mais fácil fazer Mocks/Stubs de **Interfaces, classes abstratas e métodos virtuais**, portanto **elas são mais testáveis**

**Devido à essa preocupação que o próprio projeto do EntityFramework Core criou o Provider InMemory**! Afinal, como esperar que seu framework seja amplamente adotado por projetos de grande porte se tudo que utiliza ele deixa de ser testável?

Mas, vamos às abordagens práticas!

## Abordagem "clássica" - UnitOfWork + Repositories

A [UnitOfWork](https://www.martinfowler.com/eaaCatalog/unitOfWork.html) é um dos "Enterprise Patterns". A ideia é bem simples:

> Você precisa de **uma classe que faça a gestão centralizada do estado do banco de dados durante um processo de negócio.**

Ou seja, é função da `UnitOfWork`:

1. Prover acesso aos `objects` que estão armazenados nas tabelas do seu banco
2. Prover mecanismos para salvar alterações (Commit)
3. Prover mecanismos para reverter alterações (Rollback)

Normalmente junto à `UnitOfWork` vemos um segundo padrão chamado de [Repository](https://www.martinfowler.com/eaaCatalog/repository.html). A ideia de um `Repository` é:

> **Centralizar as consultas (*Queries*) a algum `object`**, de maneira que possamos utilizar lógicas adicionais (como um Cache) em um único lugar e minimizar a quantidade de código replicado dentro do sistema.

Soa familiar com o que um `DbSet<T>` e um `DbContext` faz, não? *Pois é*! Mas chega de *foreshadowing*.

Como são patterns eles normalmente serão introduzidos como dependências da sua classes através de suas interfaces, normalmente chamadas:

1. `IUnitOfWork`, `IProjetoUnitOfWork`
2. `IMeuObjetoRepository`, `GenericRepository<T>`

Agora vamos pensar em suas vantagens e desvantagens, sempre **considerando que estamos utilizando o EntityFramework Core como nosso ORM**.

> Caso você queira saber mais sobre os Enterprise Patterns: Dê uma olhada no livro [Patterns of Enterprise Application Architecture do Martin Fowler](https://www.amazon.com.br/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420)

### Vantagens

As principais vantagens são:

1. **Abstração**: Permite que utilizemos Mocks e Stubs de maneira extremamente prática
2. **Amplamente conhecido**: Esse pattern é quase tão conhecido como Singletons
3. **Mantém o padrão de Orientação a Objeto**
4. **As interfaces podem ser exportadas facilmente para outros projetos**: Se você quiser fazer uma reutilização máxima de código você poderia declarar um repositório no seu projeto de Domínio e deixar que os diferentes runtimes façam a implementação.

Para mim as principais vantagens são #1 e #4. Especialmente se pensarmos em utilizar uma abordagem que preza pela reutilização: A mesma interface pode ser implementada no frontend com acesso a uma API REST e no backend com acesso a um banco relacional!

### Desvantagens

1. **Mais** uma classe para **Manutenção**
2. **Difícil de adicionar novas funcionalidade**: Como você deve alterar a assinatura da Interface (ou criar uma nova) cada nova query é um novo método
3. **Difícil de compor queries**: Se você permite múltiplos filtros terá de ter métodos com inúmeros parâmetros!
4. **Utilização de `generics` orientados a Copy-Paste**: Essa acho que é o maior downside que vi para a abordagem até hoje! Inúmeras vezes você vê o mesmo código colado em diferentes projetos para criar um `GenericRepository<T>` que alivia alguns dos problemas acima
5. **Quebra do próprio Pattern**: Para tentar balancear com a nova dependência é comum ver Repositories que retornam DTOs, fazem alterações no banco, etc...

Para mim as principais desvantagens são as #1, #2 e #5. Eu mesmo já fiz repositories que fazem mais que consultar e já tive de quebrar interfaces por causa de um parâmetro a mais de filtro.

### Exemplos

O seguinte snippet contém uma implementação (feita para exemplo apenas, bem incompleta!) de uma *Unit of Work* para o Cell CMS:

```{csharp}
/// <summary>
/// Descreve métodos para uma UnitOfWork do CellCMS.
/// </summary>
public interface ICellCMSUnitOfWork
{
    IRepository<Feed> Feeds { get; }

    Task CommitChanges();
}

/// <summary>
/// Descreve métodos para um repositório.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IRepository<T> where T: class
{
    Task<IEnumerable<T>> ListAll();
}

/// <summary>
/// Implementação genérica de um repository.
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericRepository<T> : IRepository<T> where T : class
{
    private DbSet<T> _set;

    public GenericRepository(CellContext context)
    {
        _set = context.Set<T>();
    }
    public async Task<IEnumerable<T>> ListAll() => await _set.ToListAsync();
}

/// <summary>
/// Simulação de uma Unit of Work para o CELL CMS.
/// </summary>
public class CellCMSUnitOfWork : ICellCMSUnitOfWork
{
    private readonly CellContext _context;
    public IRepository<Feed> Feeds { get; private set; }
    public CellCMSUnitOfWork(CellContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        Feeds = new GenericRepository<Feed>(context);
    }

    public Task CommitChanges() => _context.SaveChangesAsync();
}
```

## Abordagem funcional - Extension Methods

Primeiramente um disclaimer: **Essa solução é boa para o universo .NET, onde temos suporte a Extension Methods**.

Um `extension method` é um **método que adiciona funcionalidade a uma classe/interface existente sem precisar alterar o código original**.

Boa parte das operações `LINQ` são extension methods, assim como maior parte das opções do próprio EntityFrameworkCore! Sem acesso ao código podemos confiar no `Intellisense` para mostrar quando estamos lidando com um método que foi adicionado a um tipo por `extension method`, note o *ícone diferente* nesta imagem:

![Extension Method icon on intellisense](https://dev-to-uploads.s3.amazonaws.com/i/7z89afj8zim4ztqic16n.png)

### Extension Methods 101

> Mas Rodolpho, como posso fazer um `extension method`?

É mais simples do que parece! Você só precisa:

1. De uma `static class` onde seu método será armazenado
2. De acesso aos métodos do `Tipo` a ser estendido
3. Criar um método `static` onde o primeiro parâmetro recebe a `keyword` especial `this`.

Por exemplo, os seguintes `extension method` permitem que eu serialize um objeto para JSON, recupere o mesmo objeto a partir de um JSON e realize uma cópia do objeto através de serialização:

> Note que consegui estender um `generic T` e uma `string` e, nas últimas 3 linhas, eles são chamados como se fosse realmente parte do tipo `Feed` e `string`.

```{csharp}
public static class MyExtensions
{
    /// <summary>
    /// Serializa um objeto para um JSON.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string Dump<T>(this T obj) where T : new()
    {
        return JsonSerializer.Serialize(obj);
    }

    /// <summary>
    /// Restaura um objeto de a partir de um JSON.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="t"></param>
    /// <returns></returns>
    public static T Restore<T>(this string t) where T : new()
    {
        return JsonSerializer.Deserialize<T>(t);
    }

    /// <summary>
    /// Clona um objeto.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="from"></param>
    /// <returns></returns>
    public static T Clone<T>(this T from) where T : new()
    {
        return from.
            Dump().
            Restore<T>();
    }
}

// Usando os extension methods
var cobaia = new Feed();
var copy = cobaia.Clone();
Debug.Assert(copy != null);
```

Como o Visual Studio mostraria que estou usando uma extension:

![Utilizando um extension method com Intellisense](https://dev-to-uploads.s3.amazonaws.com/i/d1dihmzpdwh7roj0wi5w.gif)

### Vantagens

1. **Testável por unidade**: Como fazemos extensão de um `IQueryable<T>` podemos testar através de uma `List<T>`
2. **Testável por integração**: Novamente, como estendemos `IQueryable<T>` podemos testar através de um `DbSet<T>` do EFCore
3. **Armazenável próximo ao Model**: Se você curte Clean Code, podemos deixar estas queries no mesmo arquivo que a classe do Model!
4. **Permite composição**: Podemos chamar filtros após filtros conforme nossa necessidade, sempre precisar de um novo método para cada nova combinação de filtros
5. **Não duplica o pattern `UoW` + `Repository`** em sua aplicação.

Todos são pontos bem legais, na minha opinião, mas vou dar uma ênfase no #5 e faço isso pois **o próprio EntityFramework Core é uma implementação deste pattern**!

O `DbContext` que herdamos é uma `UnitOfWork` de cara por nos permitir **criar transações, realizar commits e rollbacks**. E cada **`DbSet<T>` também é repositório pois nos permite interagir com o objetos armazenados no banco!**

A própria [documentação do EntityFrameworkCore](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext?view=efcore-5.0) explica:

> Uma instância do DbContext representa uma sessão com o banco de dados e pode ser utilizado para consultar e salvar instâncias de suas entidades. O DbContext é uma combinação dos patterns Unit Of Work e Repository. *(tradução livre pelo autor)*

Com base nisso podemos concluir que:

> Uma `UnitOfWork` sobre um `DbContext` é **uma abstração feita sobre uma abstração!**

### Desvantagens

1. **Menos reutilizável longe do EFCore**: Como dependemos do `IQueryable<T>` fica mais complicado reutilizar essas queries em um mobile Xamarin, por exemplo
2. **Pouca familiaridade com `extension methods`**: Apesar de estarem ai a anos nem todo desenvolvedor já teve de escrever seu próprio `extension method`
3. **Dependendo do `namespace` podem causar confusão**: Extension methods são carregados por `namespace` de uma única vez. Você não consegue escolher quais métodos serão importados.

### Exemplos

O seguinte snippet contém algumas das queries do Cell CMS que migrei para este padrão e alguns testes mostrando como elas podem ser utilizadas, como se comportam com os diferentes _datasets_ e _chains_!

```{csharp}
/// <summary>
/// Queries relacionadas a Feeds.
/// </summary>
public static class FeedQueries
{
    /// <summary>
    /// Obtem todos os feeds.
    /// </summary>
    /// <param name="feeds"></param>
    /// <returns></returns>
    public static IQueryable<Feed> AllFeeds(this IQueryable<Feed> feeds) => feeds;

    /// <summary>
    /// Filtra feeds com base no ID.
    /// </summary>
    /// <param name="feeds"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    public static IQueryable<Feed> WithId(this IQueryable<Feed> feeds, int id)
    {
        return feeds
            .Where(f => f.Id.Equals(id));
    }

    /// <summary>
    /// Filtra feeds com base no Nome.
    /// </summary>
    /// <param name="feeds"></param>
    /// <param name="name"></param>
    /// <returns></returns>
    public static IQueryable<Feed> FilterByNome(this IQueryable<Feed> feeds, string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return feeds;
        }

        return feeds
            .Where(f => f.Nome.Contains(name, System.StringComparison.InvariantCultureIgnoreCase));
    }
}

// Testes
[Theory, CreateData]
[Trait(TraitsConstants.Label.Name, TraitsConstants.Label.Values.Domain)]
public void WithNome_EmptyData_ReturnsEmpty(string nome)
{
    // Arrange
    var subject = _emptyResult.AsQueryable();

    // Act
    var result = subject
        .FilterByNome(nome);

    // Assert
    result.Should().BeEmpty();
}

[Theory, CreateData]
[Trait(TraitsConstants.Label.Name, TraitsConstants.Label.Values.Domain)]
public void WithNome_WithDataAndValidName_ReturnsData(IEnumerable<Feed> feeds, Feed extraFeed)
{
    // Arrange
    var nome = extraFeed.Nome;
    var subject = feeds
        .Concat(new List<Feed>() { extraFeed })
        .AsQueryable();

    // Act
    var result = subject
        .FilterByNome(nome);

    // Assert
    result.Should().NotBeEmpty();
}

[Theory, CreateData]
[Trait(TraitsConstants.Label.Name, TraitsConstants.Label.Values.Domain)]
public void WithNome_WithDataAndInvalidName_ReturnsData(IEnumerable<Feed> feeds, [Frozen] string fixedName)
{
    // Arrange
    var nome = $"{fixedName}{Guid.NewGuid()}";
    var subject = feeds.AsQueryable();

    // Act
    var result = subject
        .FilterByNome(nome);

    // Assert
    result.Should().BeEmpty();
}

[Theory, CreateData]
[Trait(TraitsConstants.Label.Name, TraitsConstants.Label.Values.Domain)]
public void WithIdAndNome_WithDataAndValidNameAndValidID_ReturnsData(IEnumerable<Feed> feeds, Feed extraFeed)
{
    // Arrange
    var nome = extraFeed.Nome;
    var id = extraFeed.Id;
    var subject = feeds
        .Concat(new List<Feed>() { extraFeed })
        .AsQueryable();

    // Act
    var result = subject
        .WithId(id)
        .FilterByNome(nome);

    // Assert
    result.Should().NotBeEmpty();
}
```

## Conclusão

Eu, pessoalmente, sempre utilizei o padrão `UnitOfWork` + `Repository`. Porém, ao descobrir que os `extension methods` poderiam me dar a mesma funcionalidade mas com um menor *overhead* **passei a preferir `extension methods`.**

Caso ainda não esteja convencido dê uma olhada nos testes e refactors feitos no Cell CMS, que agora está seguindo este padrão, para basear sua decisão!

No fundo essa decisão é uma questão de gosto pessoal. Eu prefiro ser pragmático e minimizar a quantidade de coisas que preciso dar manutenção e testar. Mas para seu projeto/equipe pode ser que o padrão `UnitOfWork` seja melhor aceito!

O post de hoje vai ficando por aqui, espero que tenha dado o que pensar! O próximo post provavelmente será sobre `Analyzers` ou *Configuração de um Ambiente Windows para desenvolvimento*!

Fiquem ligados para o próximo, obrigado por lerem e até então!