ASP.NET Core - Dapper com Repository Pattern e Unit Of Work


   Neste tutorial, veremos como usar o Dapper na ASP.NET Core junto com um repositório genérico usando a Onion Architecture.

O Dapper é uma biblioteca de mapeamento objeto-relacional (ORM) de código aberto para .NET, que permite trabalhar com bancos de dados relacionais usando objetos e métodos familiares do C#.



Ao contrário de outros ORMs que abstraem completamente o SQL, o Dapper trabalha com o SQL diretamente, permitindo que o desenvolvedor escreva consultas SQL personalizadas e otimizadas, em vez de depender de consultas geradas automaticamente pelo ORM. O Dapper é conhecido por sua alta performance, pois utiliza técnicas de mapeamento de dados de baixo nível e é otimizado para minimizar a sobrecarga de processamento.

Vamos mostrar como usar o Dapper em um projeto ASP .NET Core em um repositório genérico e também vamos implementar o padrão Unit Of Work. Seguindo as boas práticas vamos usar a arquitetura Cebola ou Onion no projeto onde vamos gerenciar informações de produtos.

A Onion Architecture (também conhecida como Arquitetura Cebola) é um padrão de arquitetura de software que propõe uma abordagem em camadas para o desenvolvimento de aplicações. A ideia é que cada camada da arquitetura seja independente das outras, sendo que as camadas internas contêm a lógica de negócios e as externas fornecem interfaces para outras camadas ou para o mundo exterior.

A arquitetura é chamada de "Cebola" porque as camadas são organizadas em círculos concêntricos, como as camadas de uma cebola. Na camada mais interna, temos as entidades de negócios e a lógica de domínio, que representam o núcleo da aplicação. Em seguida, há camadas de serviços de aplicação, interfaces de usuário, infraestrutura e, finalmente, a camada externa, que representa o ambiente externo à aplicação, como bancos de dados, sistemas de arquivos e outros serviços externos.

recursos usados:

Criando o banco de dados no SQL Server

Vamos criar o banco de dados Cadastro e a tabela Produtos que iremos usar no projeto no SQL Server usando o SQL Server Management Studio :

1- Abra o SQL Management Studio e conecte-se ao seu banco de dados local (SQL Express)

2- Clique com o botão direito do mouse na pasta Bancos de Dados e selecione Novo Banco de Dados...

3- Digite um nome de Banco de Dados - Cadastro - e finalize clicando em OK.

Clique com o botão direito do mouse em seu novo banco de dados e selecione Nova consulta.

Na janela em branco vamos executar o comando SQL para criar  tabela Produtos conforme o código abaixo:

CREATE TABLE Produtos(
   Id
int IDENTITY(1,1) NOT NULL,
   Nome
nvarchar(50) NOT NULL,
   CodigoBarras
nvarchar(50) NOT NULL,
   Descricao
nvarchar(max) NOT NULL,
   Preco
decimal(18, 2) NOT NULL,
   IncluidoEm
datetime NOT NULL,
   ModificadoEm
datetime NULL,
CONSTRAINT PK_Products PRIMARY KEY CLUSTERED
(

  Id
ASC
)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
 
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

A seguir clique em Executar para criar a tabela.

Ao final você deverá ver no seu banco de dados a tabela Produtos com a seguinte estrutura:



Criando o projeto

Abra o VS 2022 Community e crie um novo projeto usando o template Blank Solution com o nome ApiDapper.

A seguir vamos incluir na solução os seguinte projetos usando o template Class Library :

Para concluir inclua o projeto WEB API na solução usando o template ASP.NET COre Web API:

Com as seguintes configurações:

Ao final teremos a solução e os projetos conforme mostrado abaixo:

Para simplificar o projeto a camada Application que deveria conter os serviços que orquestram os objetos do domínio contém apenas as pastas Services e Interfaces onde definimos um esboço do serviço que poderia ser usado no futuro.

As interfaces para produto, repositório e unit of work foram definidas na pasta Interfaces da camada Domain  pois são parte da infraestrutura necessária para que a camada de domínio possa se comunicar com a camada de persistência de dados sem violar a separação de responsabilidades e a abstração do domínio.

Dessa forma, a implementação dos repositórios e da unidade de trabalho deve ser feita na camada de infraestrutura, e as interfaces devem ser definidas na camada de domínio para que a camada de aplicação possa utilizá-las sem ter que conhecer os detalhes da implementação na camada de infraestrutura.

Desta forma na camada de aplicação, você pode definir classes que implementam os casos de uso da aplicação. Essas classes fazem uso das interfaces dos repositórios e da unidade de trabalho definidos na camada de domínio para interagir com a camada de persistência de dados implementada na camada de infraestrutura.

Como o objetivo do artigo é mostrar o uso do Dapper eu não vou fazer essa implementação na camada Application.

No projeto Domain vamos criar a pasta Entities e nesta pasta criar a classe Produto:

public class Produto
{
    public int Id { get; set; }
    public string? Nome { get; set; }
    public string? Descricao { get; set; }
    public string? CodigoBarras { get; set; }
    public decimal Preco { get; set; }
    public DateTime IncluidoEm { get; set; }
    public DateTime ModificadoEm { get; set; }
}

Neste artigo, para simplificar o projeto eu não estou criando um construtor na entidade, não estou definindo os acessores set como private e também não estou fazendo a validação do domínio. Esses detalhes devem sempre ser levados em conta em um projeto completo e de produção.

Vamos criar a pasta Interfaces na camada Domain e criar nesta pasta as interfaces IRepository, IProdutoRepository e IUnitOfWork :

1- IRepository

public interface IRepository<T> where T : class
{
    Task<T> GetPorIdAsync(int id);
    Task<IReadOnlyList<T>> GetTodosAsync();
    Task<int> AdicionarAsync(T entity);
    Task<int> AtualizarAsync(T entity);
    Task<int> DeletarAsync(int id);
}

2- IProdutoRepository

public interface IProdutoRepository : IRepository<Produto>
{
}

3- IUnitOfWork

public interface IUnitOfWork
{
    IProdutoRepository Produtos { get; }
}

O projeto Domain não deve conter referêcia a nenhum projeto.

Na camada Application vamos apenas criar as pastas Interfaces e Services e nestas pastas vamos criar a interface IProdutoService e a classe concreta ProdutoService sem implementação para simplificar a abordagem como já foi explicado.

Na camada Infrastructure  vamos incluir os seguintes pacotes Nuget:

  1. Dapper
  2. Microsoft.Extensions.Configuration
  3. Microsoft.Extensions.DependencyInjection.Abstractions
  4. System.Data.SqlClient

O projeto Infrastructure dever conter uma referência ao projeto Domain.

A seguir vamos criar a pasta Repositories e nesta classe criar as seguintes classes:

1- ProdutoRepository

using ApiDapper.Domain.Abstractions;
using ApiDapper.Domain.Entities;
using Dapper;
using Microsoft.Extensions.Configuration;
using System.Data.SqlClient;

namespace ApiDapper.Infrastructure.Repositories;

public class ProdutoRepository : IProdutoRepository
{
    private readonly IConfiguration configuration;
    public ProdutoRepository(IConfiguration configuration)
    {
        this.configuration = configuration;
    }
    public async Task<int> AdicionarAsync(Produto produto)
    {
        produto.IncluidoEm = DateTime.Now;
        var sql = "Insert into Produtos (Nome,Descricao,CodigoBarras,Preco,IncluidoEm) VALUES (@Nome,@Descricao,@CodigoBarras,@Preco,@IncluidoEm)";
        using (var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection")))
        {
            connection.Open();
            var result = await connection.ExecuteAsync(sql, produto);
            return result;
        }
    }
    public async Task<int> DeletarAsync(int id)
    {
        var sql = "DELETE FROM Produtos WHERE Id = @Id";
        using (var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection")))
        {
            connection.Open();
            var result = await connection.ExecuteAsync(sql, new { Id = id });
            return result;
        }
    }
    public async Task<IReadOnlyList<Produto>> GetTodosAsync()
    {
        var sql = "SELECT * FROM Produtos";
        using (var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection")))
        {
            connection.Open();
            var result = await connection.QueryAsync<Produto>(sql);
            return result.ToList();
        }
    }
    public async Task<Produto> GetPorIdAsync(int id)
    {
        var sql = "SELECT * FROM Produtos WHERE Id = @Id";
        using (var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection")))
        {
            connection.Open();
            var result = await connection.QuerySingleOrDefaultAsync<Produto>(sql, new { Id = id });
            return result;
        }
    }
    public async Task<int> AtualizarAsync(Produto produto)
    {
        produto.ModificadoEm = DateTime.Now;
        var sql = "UPDATE Produtos SET Nome = @Nome, Descricao = @Descricao,
                   CodigoBarras = @CodigoBarras, Preco = @Preco, ModificadoEm = @ModificadoEm  WHERE Id = @Id";

        using (var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection")))
        {
            connection.Open();
            var result = await connection.ExecuteAsync(sql, produto);
            return result;
        }
    }
}

2- UnitOfWork

using ApiDapper.Domain.Abstractions;
namespace ApiDapper.Infrastructure.Repositories;
public class UnitOfWork : IUnitOfWork
{
    public UnitOfWork(IProdutoRepository productRepository)
    {
        Produtos = productRepository;
    }
    public IProdutoRepository Produtos { get; }
}

Aqui temos que a classe UnitOfWork está explicitamente acoplada ao IProdutoRepository. Isso significa que, se você decidir adicionar mais repositórios no futuro (por exemplo, IClienteRepository, IVendaRepository, etc.), precisará modificar a classe UnitOfWork para injetar esses repositórios adicionais e isso viola o princípio Open-Closed do SOLID, que preconiza que as classes devem estar abertas para extensão, mas fechadas para modificação.

Assim uma melhoria para esta implementação seria usar a técnica de injeção de dependência automática. Isso pode ser feito configurando o contêiner de injeção de dependência para registrar automaticamente os repositórios necessários no escopo da unidade de trabalho.

Na camada ApiProdutos vamos criar na pasta Controllers o controlador ProdutosController :

using ApiDapper.Domain.Abstractions;
using ApiDapper.Domain.Entities;
using Microsoft.AspNetCore.Mvc;

namespace ApiProdutos.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    private readonly IUnitOfWork unitOfWork;

    public ProdutosController(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var data = await unitOfWork.Produtos.GetTodosAsync();
        return Ok(data);
    }
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var data = await unitOfWork.Produtos.GetPorIdAsync(id);
        if (data == null) return Ok();
        return Ok(data);
    }
    [HttpPost]
    public async Task<IActionResult> Add(Produto product)
    {
        var data = await unitOfWork.Produtos.AdicionarAsync(product);
        return Ok(data);
    }
    [HttpDelete]
    public async Task<IActionResult> Delete(int id)
    {
        var data = await unitOfWork.Produtos.DeletarAsync(id);
        return Ok(data);
    }
    [HttpPut]
    public async Task<IActionResult> Update(Produto product)
    {
        var data = await unitOfWork.Produtos.AtualizarAsync(product);
        return Ok(data);
    }
}

Este projeto deverá ter uma referência aos projetos Domain e Infrastructure.

Agora só falta definir a string de conexão no arquivo appsettings.json:

"ConnectionStrings": {

"DefaultConnection": "Data Source=.;Initial Catalog=Cadastro;Integrated Security=True;TrustServerCertificate=True;"

},
...

Executando o projeto iremos obter o resultado abaixo:

Vamos criar alguns produtos acionando o endpoint Post api/Produtos :

A seguir vamos acionar o endpoint GET api/produtos :

Podemos também atualizar e excluir um produto usando os endpoints implementados com a ajuda do Dapper e usando os padrões Repository e Unit Of Work.

Pegue o projeto aqui : ApiDapper.zip (sem as referências)

E estamos conversados...

"Uns confiam em carros e outros em cavalos, mas nós faremos menção do nome do Senhor nosso Deus."
Salmos 20:7

Referências:


José Carlos Macoratti