Repository Pattern em aplicações .NET

O Repository Pattern no .NET é um padrão de design que separa a lógica de acesso a dados da lógica de negócios da aplicação. Graças à orientação a objetos do C# e ao container de injeção de dependência do ASP.NET Core, aplicar esse padrão costuma ser simples e direto.

A ideia central é criar uma camada de abstração entre a aplicação e a fonte de dados. Essa fonte pode ser um banco relacional, um web service ou qualquer outro mecanismo de armazenamento.

Na prática, você alcança isso com interfaces, ou classes abstratas, que definem os métodos para acessar e manipular os dados. Assim, o restante do código conversa com o contrato, e não com o banco.

Neste guia, você vai montar uma solução com três projetos, criar entidade, contrato, repositório, casos de uso e controller. Além disso, vai entender as vantagens e os limites do padrão.

O que é o Repository Pattern no .NET

Uma boa forma de entender o padrão é imaginar um bibliotecário em uma biblioteca gigantesca. O leitor representa a aplicação, e a biblioteca representa a fonte de dados.

O bibliotecário estabelece uma linguagem comum e simplificada para pedir livros. Dessa forma, o leitor não precisa saber em qual prateleira, corredor ou andar cada obra está guardada.

No código, essa linguagem comum são as interfaces. Elas definem como os dados podem ser acessados, enquanto a implementação concreta esconde os detalhes da fonte.

Martin Fowler descreve o repositório como um intermediário entre o domínio e o mapeamento de dados. Segundo ele, o repositório se comporta como uma coleção de objetos em memória.

O guia de arquitetura de microsserviços da Microsoft reforça essa definição. O material afirma que repositórios centralizam a funcionalidade comum de acesso a dados e desacoplam o domínio da tecnologia de persistência.

Por que usar o Repository Pattern no .NET

Existem quatro motivos principais para adotar o padrão em aplicações .NET. Cada um deles resolve um problema comum em projetos que crescem.

Separação de responsabilidades

O padrão separa com clareza as responsabilidades de acesso a dados das responsabilidades de negócio. Com isso, o código fica mais modular e mais fácil de compreender e manter.

Esse princípio conversa diretamente com o SRP dos princípios SOLID. Cada classe passa a ter um único motivo para mudar, e o Repository Pattern no .NET torna esse limite explícito no código.

Testabilidade

Com o Repository Pattern no .NET, fica mais fácil escrever testes unitários para a lógica de negócios. Isso acontece porque você pode substituir os repositórios por implementações de teste que imitam o comportamento do banco.

A própria documentação do EF Core recomenda o padrão quando você precisa de dublês de teste. Segundo a Microsoft, ele é a única abordagem que permite simular a camada de dados de forma completa e confiável.

Flexibilidade

O padrão também facilita a troca da tecnologia de persistência. Por exemplo, você pode migrar do SQL Server para o MongoDB reescrevendo apenas a implementação do repositório.

Os casos de uso e os controllers continuam iguais, porque dependem somente da interface. Ainda assim, a migração exige esforço, já que consultas e modelagem mudam entre bancos relacionais e NoSQL.

Padronização do acesso a dados

O Repository Pattern padroniza a forma como a aplicação acessa e manipula os dados. Em outras palavras, toda nova entidade segue o mesmo caminho.

Essa padronização acelera a criação de novas funcionalidades e casos de uso. Afinal, cada repositório existente serve como molde para o próximo.

Por isso, o Repository Pattern no .NET também funciona como um guia de convenções para o time.

Organização da solução para o Repository Pattern no .NET

Como o Repository Pattern no .NET ajuda a separar responsabilidades, vale criar a solução já pensando nisso. O exemplo a seguir usa três projetos diferentes.

Projeto RepoPatternExample.Api

Esse é o projeto ASP.NET Core, onde fica a pasta Controllers. Nele você cria as rotas com os métodos GET, POST, PUT e DELETE.

Além disso, esse projeto contém o arquivo Program.cs. É ali que a aplicação inicia e onde você registra as configurações adicionais por meio do builder.

Projeto RepoPatternExample.Application

Esse é um projeto de biblioteca de classes que abriga a pasta UseCases. Nele você define e implementa as operações que a aplicação pode realizar.

No Repository Pattern no .NET, essa camada conhece apenas os contratos dos repositórios. Ela nunca acessa o banco diretamente.

Projeto RepoPatternExample.Infrastructure

Também é uma biblioteca de classes. Contudo, aqui ficam as pastas Contracts, com as interfaces, Entities, com as entidades, e Repositories, com as classes que interagem com o banco.

Para que os projetos se enxerguem, você precisa adicionar as referências entre eles. No Visual Studio, clique com o botão direito em Dependências, escolha Adicionar referência de projeto e marque os projetos necessários.

Pelo terminal, o mesmo resultado sai com o comando dotnet add reference. Por exemplo, a API referencia a camada de aplicação, que por sua vez referencia a infraestrutura.

# Executado na pasta da API
dotnet add reference ../RepoPatternExample.Application
dotnet add reference ../RepoPatternExample.Infrastructure

# Executado na pasta da camada de aplicação
dotnet add reference ../RepoPatternExample.Infrastructure

Entidade e contrato do Repository Pattern no .NET

Todo Repository Pattern no .NET começa por duas peças: a entidade e o contrato. Por ser um projeto de exemplo, a solução tem apenas uma entidade simples chamada User, que fica na pasta Entities.

namespace RepoPatternExample.Infrastructure.Entities;

public class User
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

Em seguida, a pasta Contracts recebe a interface que representa o repositório para o restante da aplicação.

using RepoPatternExample.Infrastructure.Entities;

namespace RepoPatternExample.Infrastructure.Contracts;

public interface IUserRepository
{
    User Create(User user);
    User? GetById(Guid userId);
}

Por convenção, o nome de uma interface no .NET começa com a letra I. Assim, a interface do repositório UserRepository se chama IUserRepository.

Vale um comentário sobre o local do contrato. O guia da Microsoft recomenda colocar as interfaces dos repositórios na camada de domínio, mas manter tudo na infraestrutura simplifica um exemplo didático como este.

Adicionando o Entity Framework Core e o DbContext

Antes de criar o UserRepository, você precisa de um DbContext. Afinal, neste exemplo o Repository Pattern no .NET usa o Entity Framework Core por baixo dos panos, então adicione o pacote ao projeto de infraestrutura.

Existem dois caminhos para essa instalação. O primeiro é abrir o Gerenciador de Pacotes do NuGet no projeto RepoPatternExample.Infrastructure, buscar Microsoft.EntityFrameworkCore e instalar a versão mais recente.

O segundo é usar o terminal, dentro da pasta do projeto. Depois, adicione também o provedor do banco escolhido, como o SQLite deste exemplo.

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

Com os pacotes instalados, crie a classe de configuração do banco. Neste exemplo, ela se chama RepoPatternDbContext.

using Microsoft.EntityFrameworkCore;
using RepoPatternExample.Infrastructure.Entities;

namespace RepoPatternExample.Infrastructure.Repositories;

public class RepoPatternDbContext : DbContext
{
    public RepoPatternDbContext(DbContextOptions<RepoPatternDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
}

Repare que a classe não define nenhuma string de conexão. Isso é intencional, porque a configuração da conexão acontece na inicialização da aplicação, como você verá adiante.

O construtor que recebe DbContextOptions<RepoPatternDbContext> é justamente o que permite essa configuração externa. Segundo a documentação do EF Core, é por ele que o AddDbContext entrega as opções ao contexto.

Implementando o repositório do Repository Pattern no .NET

Ainda na pasta Repositories, crie a classe UserRepository. Por convenção, o nome do repositório combina o nome da entidade com o sufixo Repository.

Essa classe é a peça central do Repository Pattern no .NET, porque concentra todo o acesso ao banco.

using Microsoft.EntityFrameworkCore;
using RepoPatternExample.Infrastructure.Contracts;
using RepoPatternExample.Infrastructure.Entities;

namespace RepoPatternExample.Infrastructure.Repositories;

public class UserRepository : IUserRepository
{
    private readonly RepoPatternDbContext _context;

    public UserRepository(RepoPatternDbContext context) => _context = context;

    public User Create(User user)
    {
        var entity = new User
        {
            Name = user.Name,
            Email = user.Email
        };

        _context.Users.Add(entity);
        _context.SaveChanges();

        return entity;
    }

    public User? GetById(Guid userId)
        => _context.Users
            .AsNoTracking()
            .FirstOrDefault(user => user.Id == userId);
}

Essa classe é um pouco maior, então vale analisar por partes. Primeiro, ela implementa a interface IUserRepository, o que a obriga a oferecer os métodos Create e GetById.

Depois, ela recebe o RepoPatternDbContext pelo construtor e guarda a instância em um campo privado. Quem fornece essa instância é o container de injeção de dependência.

Por fim, os dois métodos concentram a lógica de persistência. O GetById usa AsNoTracking, porque uma consulta de leitura não precisa que o EF Core rastreie alterações.

Note também que o método retorna User?. Assim, o chamador sabe que o usuário pode não existir e precisa tratar esse caso.

Registrando a conexão e o repositório

Neste ponto, pode parecer que basta criar o caso de uso e chamá-lo no controller. Antes disso, porém, falta mostrar onde a conexão com o banco realmente acontece.

No projeto RepoPatternExample.Api, abra o arquivo Program.cs e adicione os registros abaixo.

builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<CreateUserUseCase>();
builder.Services.AddScoped<GetUserUseCase>();

builder.Services.AddDbContext<RepoPatternDbContext>(options =>
{
    // Troque pelo provedor e pela string de conexão do seu banco
    options.UseSqlite("Data Source=app.db");
});

A primeira linha informa à aplicação que, sempre que um componente precisar de IUserRepository, o container deve fornecer uma instância de UserRepository. Sem esse registro, a aplicação não sabe qual implementação entregar.

Em outras palavras, a injeção de dependência é o que faz o Repository Pattern no .NET funcionar.

As duas linhas seguintes registram os casos de uso. Isso é necessário porque o controller vai recebê-los por injeção de dependência.

O bloco final é a conexão com o banco. Ela fica no Program.cs porque o DbContext precisa estar registrado como serviço quando a aplicação inicia.

Segundo a documentação do EF Core, o AddDbContext registra o contexto com tempo de vida scoped por padrão. Na prática, cada requisição HTTP recebe a sua própria instância, descartada ao final da requisição.

Se quiser entender melhor esses tempos de vida, o artigo sobre AddTransient, AddScoped e AddSingleton explica as diferenças em detalhes.

Antes de executar, o banco também precisa existir. O artigo sobre migrations com EF Core mostra como gerar e aplicar o esquema.

Criando os casos de uso com o Repository Pattern no .NET

Com a infraestrutura pronta, siga para a camada de aplicação. No projeto RepoPatternExample.Application, crie as classes CreateUserUseCase e GetUserUseCase dentro da pasta UseCases.

Por convenção, o nome do caso de uso descreve a ação que ele executa seguida do sufixo UseCase. No Repository Pattern no .NET, os casos de uso recebem o repositório pela interface, nunca pela classe concreta.

using RepoPatternExample.Infrastructure.Contracts;
using RepoPatternExample.Infrastructure.Entities;

namespace RepoPatternExample.Application.UseCases;

public class CreateUserUseCase
{
    private readonly IUserRepository _repository;

    public CreateUserUseCase(IUserRepository repository) => _repository = repository;

    public User Execute(User user) => _repository.Create(user);
}
using RepoPatternExample.Infrastructure.Contracts;
using RepoPatternExample.Infrastructure.Entities;

namespace RepoPatternExample.Application.UseCases;

public class GetUserUseCase
{
    private readonly IUserRepository _repository;

    public GetUserUseCase(IUserRepository repository) => _repository = repository;

    public User? Execute(Guid userId) => _repository.GetById(userId);
}

Ao observar as duas classes, dois pontos chamam a atenção. Primeiro, os casos de uso ficaram enxutos, porque a lógica de persistência pertence ao UserRepository.

Segundo, graças ao registro com AddScoped, os casos de uso não dependem da classe concreta UserRepository. Eles dependem apenas da abstração IUserRepository.

Criando o controller

Agora, no projeto RepoPatternExample.Api, crie o UsersController dentro da pasta Controllers.

using Microsoft.AspNetCore.Mvc;
using RepoPatternExample.Application.UseCases;
using RepoPatternExample.Infrastructure.Entities;

namespace RepoPatternExample.Api.Controllers;

[ApiController]
[Route("v1/users")]
public class UsersController : ControllerBase
{
    [HttpPost]
    public IActionResult Create(
        [FromServices] CreateUserUseCase useCase,
        [FromBody] User user)
    {
        var response = useCase.Execute(user);
        return Created($"v1/users/{response.Id}", response);
    }

    [HttpGet("{userId:guid}")]
    public IActionResult GetById(
        [FromServices] GetUserUseCase useCase,
        [FromRoute] Guid userId)
    {
        var response = useCase.Execute(userId);
        return response is null ? NotFound() : Ok(response);
    }
}

O controller é simples e apenas chama os casos de uso. O POST cria o usuário e devolve o endereço do recurso, enquanto o GET retorna o usuário ou um 404 quando ele não existe.

Repare também que o controller não conhece o DbContext.

Esse é um dos efeitos mais visíveis do Repository Pattern no .NET. Em uma abordagem sem repositório, seria comum injetar o contexto diretamente no controller.

Aqui, no entanto, o container entrega o RepoPatternDbContext ao UserRepository, que por sua vez chega ao caso de uso pela interface. Com isso, a aplicação aplica o Repository Pattern no .NET de ponta a ponta.

Vantagens do Repository Pattern no .NET

Uma dúvida comum surge neste ponto. Afinal, todas essas camadas não seriam over-engineering para uma aplicação tão pequena?

Para um exemplo com uma entidade, talvez sim. Contudo, as vantagens do Repository Pattern no .NET aparecem à medida que o sistema cresce.

Desacoplamento

Quando o caso de uso depende da abstração IUserRepository, ele não fica preso à implementação concreta UserRepository. Portanto, você pode trocar a implementação sem modificar o caso de uso.

Por exemplo, uma nova classe que se conecta a outro banco pode implementar a mesma interface. Basta alterar o registro no Program.cs, e o restante da aplicação continua igual.

O artigo sobre injeção de dependência aprofunda esse mecanismo, que sustenta todo o desacoplamento.

Clareza e manutenção

Interfaces deixam o código mais claro, porque expressam de forma objetiva as dependências externas de cada caso de uso. Quem lê o construtor já entende do que a classe precisa.

Consequentemente, a manutenção e o reuso ficam mais simples ao longo do tempo. Esse ganho é um dos argumentos mais fortes a favor do Repository Pattern no .NET em equipes grandes.

Testes isolados com o Repository Pattern no .NET

Com interfaces, criar um mock para um teste unitário fica muito mais fácil. Ou seja, você testa o caso de uso de forma isolada, sem tocar no banco de dados nem em outra fonte externa.

A documentação do EF Core faz, porém, uma ressalva importante. Testes contra o banco real continuam necessários, porque as consultas do repositório também precisam de validação.

Para revisar a base desse tema, consulte o artigo sobre fundamentos dos testes de unidade.

Limites do Repository Pattern no .NET e próximos passos

O Repository Pattern no .NET não é obrigatório. O próprio guia da Microsoft afirma que repositórios são úteis, mas não essenciais em DDD nem no desenvolvimento .NET em geral.

Vale lembrar ainda que o DbContext já implementa os padrões Repository e Unit of Work. Por isso, avalie se a camada extra realmente traz ganho para o seu contexto.

Quando muitos repositórios repetem o mesmo código, surge a tentação do repositório genérico. O artigo sobre repositórios genéricos discute os prós e contras dessa abordagem.

Por fim, este exemplo deixou de fora algumas boas práticas importantes. Entre elas estão o uso de ViewModels ou DTOs, o tratamento de exceções, métodos assíncronos e a implementação dos testes unitários.

Conclusão

O Repository Pattern no .NET cria uma camada clara entre a lógica de negócios e a fonte de dados. Com ele, cada projeto da solução assume uma responsabilidade bem definida.

Neste guia, você montou entidade, contrato, DbContext, repositório, casos de uso e controller. Além disso, viu como o container de injeção de dependência conecta todas essas peças.

Use o Repository Pattern no .NET quando o desacoplamento e a testabilidade justificarem o custo. Assim, sua aplicação ganha estrutura sem carregar camadas desnecessárias.

FAQ

O Repository Pattern é obrigatório em aplicações .NET?

Não. Ele é útil para desacoplamento e testes, mas a própria Microsoft afirma que repositórios não são essenciais no desenvolvimento .NET.

Onde fica a string de conexão quando uso repositórios?

Ela fica na configuração da aplicação, registrada no Program.cs com AddDbContext. O repositório apenas recebe o DbContext pronto por injeção de dependência.

Qual tempo de vida usar para o repositório?

Scoped, o mesmo do DbContext registrado pelo AddDbContext. Assim, cada requisição usa uma instância própria do repositório e do contexto.

O repositório deve retornar IQueryable?

Em geral, não. Retornar IEnumerable ou a própria entidade mantém a consulta encapsulada e facilita a simulação em testes.

Posso trocar de banco sem alterar os casos de uso?

Sim, desde que os casos de uso dependam apenas da interface. Basta criar uma nova implementação do repositório e alterar o registro no container.