ASP .NET Core - Criando um Repositório Assíncrono Genérico


Neste artigo veremos como criar um repositório assíncrono em uma aplicação ASP .NET Core de forma a recordar os conceitos da programação assíncrona. .

A programação assíncrona é uma técnica de programação paralela, que permite que o processo de trabalho seja executado separadamente do thread principal do aplicativo. Assim que o trabalho for concluído, ele informa ao tópico principal sobre o resultado, se foi bem-sucedido ou não.

Ao usar a programação assíncrona, podemos evitar gargalos de desempenho e melhorar a capacidade de resposta do nosso aplicativo.

Na programação assíncrona não enviamos as requisições para o servidor e a bloqueamos enquanto aguardamos as respostas (o tempo que for necessário). Agora, quando enviamos uma solicitação ao servidor, o pool de threads delega uma thread a essa solicitação.

Eventualmente, essa thread termina seu trabalho e retorna ao pool de threads, liberando-se para a próxima solicitação. Em algum momento, os dados serão obtidos do banco de dados e o resultado precisa ser enviado ao solicitante. Nesse momento, o pool de threads fornece outra thread para lidar com esse trabalho, e, uma vez que o trabalho for concluído, a thread  usada volta para o pool de threads.

Abaixo temos uma figura representando o fluxo de trabalho assíncrono :



As palavras async, await e os tipos de retorno

As palavras-chave async e await desempenham um papel crucial na programação assíncrona. Ao usar essas palavras-chave, podemos escrever facilmente métodos assíncronos sem muito esforço.

Use o modificador async para especificar que um método, uma expressão ou um método anônimo é assíncrono. Se você usar esse modificador em um método ou expressão, ele será referido como um método assíncrono.  Exemplo:

public async Task<int> MetodoAsync()
{
    //...
   
var resultado = await httpClient.GetStringAsync(requestUrl);   
    ....
}

Todo o método onde for usado a palavra async deve conter a palavra await.  A palavra-chave await oferece uma maneira sem bloqueio de iniciar uma tarefa e, em seguida, continuar a execução quando essa tarefa for concluída. Exemplo:

Se o método que a palavra-chave async modifica não contiver uma expressão ou instrução await, ele será executado de forma síncrona. Um aviso do compilador o alertará sobre quaisquer métodos assíncronos que não contenham instruções await, pois essa situação poderá indicar um erro.

Mas o que mais caracteriza os métodos assíncronos?

Devemos considerar o seguinte :

  1. A assinatura do método deve incluir o modificador async;

  2. O método deve ter um tipo de retorno da Task<TResult>, Task ou void;

  3. As declarações de método devem incluir pelo menos uma única expressão await - isso diz ao compilador que o método precisa ser suspenso enquanto a operação aguardada estiver ocupada.

  4. Por último, o nome do método deve terminar com o sufixo "async" (mesmo que isso seja mais convencional do que o necessário).

Como exemplo suponha que desejamos criar um método assíncrono para retornar todos os produtos. Para isso temos que usar a palavra-chave async e definir o tipo de retorno do método.


async Task<IEnumerable<Produto>> GetTodosProdutosAsync()  

 

Ao usar a palavra-chave async, estamos habilitando o uso da palavra-chave await e modificando a forma como os resultados do método são tratados (de síncrono para assíncrono).

A seguir veremos como criar um repositório assíncrono genérico em um projeto ASP .NET Core Web API.

Recursos usados:

Criando o projeto ASP.NET Core Web API na pasta API

Abra o VS 2022, clique em New Project e selecione o template ASP .NET Core Web API e clique em Next;0

Informe o nome ProdutosApi e clique em Next;

A seguir selecione o Framework, Authentication Type e demais configurações conforme mostrada na figura:

Obs: Note que vamos usar Controllers e não vamos usar as minimal APIs.

Clique em Create.

Com o projeto criado vamos criar as pastas Models e Repositoreis no projeto e incluir os seguintes pacotes:

Após isso teremos a seguinte estrutura no projeto :

Na pasta Models vamos criar as classes Produto e Categoria que representam o nosso modelo de domínio:

1- Categoria

public class Categoria
{
   public int CategriaId { get; set; }
   public string? Nome { get; set; }
   public string? Descricao { get; set; }

   public ICollection<Produto> Produtos { get; set; }
}

2- Produto

public class Produto
    {
        public int ID { get; set; }
        public string? Nome { get; set; }
        public string? Descricao { get; set; }
        public decimal Preco { get; set; }
        public string? ImagemUrl { get; set; }
        public DateTime DataCompra { get; set; }
        public int Estoque { get; set; }

        public int CategoriaId { get; set; }
        public Categoria Categoria { get; set; }
    }

Defini as propriedades de navegação nas entidades para que o EF Core possa inferir o relacionamento entre Categoria e Produto.

A seguir vamos criar nesta pasta a classe de contexto AppDbContext que herda de DbContext:

using Microsoft.EntityFrameworkCore;

namespace ProdutosApi.Models;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
    public DbSet<Produto>? Produtos { get; set; }
    public DbSet<Categoria>? Categorias { get; set; }
}

No arquivo appsettings.json vamos definir a string de conexão:

{
"ConnectionStrings": {
    "DefaultConnection": "Data Source=<sua_instancia>;Initial Catalog=CadastroDB;Integrated Security=True"
},
"Logging": {
  "LogLevel": {
     "Default": "Information",
         "Microsoft.AspNetCore": "Warning"
       }
},
"AllowedHosts": "*"
}

No arquivo Program vamos registrar o serviço do contexto

var conexaoDB = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<AppDbContext>(options =>
{
   options.UseSqlServer(conexaoDB);
});

Criando os Repositórios e as implementações

Agora na pasta Repositories vamos criar a interface IRepositoryBase() :

using System.Linq.Expressions;

namespace ProdutosApi.Repositories;

public interface IRepositoryBasec<T> where T : class
{
   IQueryable<T> FindAll();

   IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression);

   void Create(T entity);

   void Update(T entity);

   void Delete(T entity);
}

Neste código estamos usamos o namespace using System.Linq.Expressions que contém classes e enumerações que permitem representar expressões de código no nível da linguagem como objetos na forma de árvores de expressões;

Note que as assinaturas dos métodos FindAll e FindByCondition retornam IQueryable que nos permite anexar chamadas assíncronas a eles e também podemos aplicar expressões lambdas para filtrar e classificar os dados;

O método FindByCondidtion() retorna os dados que atendem o critério informado em tempo de execução via expressão lambada. Estamos usando o delegate Func, e aplicando o predicate expression para verificar se o dado atende o critério (retorna true ou false);

Os métodos Create, Update e Delete não modificam nenhum dado, eles apenas rastreiam as alterações em uma entidade e aguardam a execução do método SaveChanges do EF Core.

Observe que na definição da interface do repositório temos uma restrição que diz que o tipo T deve ser uma classe.  Existem vários tipos de restrições que podem ser utilizadas para limitar os tipos permitidos. Abaixo temos algumas delas:

A seguir vamos criar as interfaces para os repositórios de Categoria e Produto.

1- ICategoriaRepository

using ProdutosApi.Models;

namespace ProdutosApi.Repositories;

public interface ICategoriaRepository : IRepositoryBase<Categoria>
{
    Task<IEnumerable<Categoria>> GetAllCategoriasAsync();
    Task<Categoria> GetCategoriaByIdAsync(int Id);
    Task<Categoria> GetCategoriaWithDetailsAsync(int Id);
    void CreateCategoria(Categoria Categoria);
    void UpdateCategoria(Categoria Categoria);
    void DeleteCategoria(Categoria Categoria);
}

2- IProdutoRepository

using ProdutosApi.Models;

namespace ProdutosApi.Repositories;

public interface IProdutoRepository : IRepositoryBase<Produto>
{
    Task<bool> IsProdutoAtivo(string nome);
    Task<IEnumerable<Produto>> GetAllProdutosAsync();
    Task<Produto> GetProdutoByIdAsync(int Id);
    void CreateProduto(Produto produto);
    void UpdateProduto(Produto produto);
    void DeleteProduto(Produto produto);
}

A seguir vamos criar uma interface para agrupar esses dois repositórios:

3- IRepositoryWrapper

namespace ProdutosApi.Repositories;

public interface IRepositoryWrapper
{
    ICategoriaRepository CategoriaRepo { get; }
    IProdutoRepository ProdutoRepo { get; }
    Task SaveAsync();
}

A seguir vamos fazer as implementações destas interfaces. Para isso vamos criar uma pasta Implementations dentro da pasta Repositories.

A seguir dentro da pasta Implementations vamos criar cada implementação :

1- BaseRepository

using Microsoft.EntityFrameworkCore;
using ProdutosApi.Models;
using System.Linq.Expressions;

namespace ProdutosApi.Repositories.Implementations;

public abstract class BaseRepository<T> : IRepositoryBase<T> where T : class
{
    protected readonly AppDbContext _dbContext;
    public BaseRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IQueryable<T> FindAll()
    {
        return _dbContext.Set<T>().AsNoTracking();
    }

    public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression)
    {
        return _dbContext.Set<T>()
               .Where(expression).AsNoTracking();
    }

    public void Create(T entity)
    {
        _dbContext.Set<T>().Add(entity);
    }

    public void Update(T entity)
    {
        //_dbContext.Entry(entity).State = EntityState.Modified;
        _dbContext.Set<T>().Update(entity);
    }

    public void Delete(T entity)
    {
        _dbContext.Set<T>().Remove(entity);
    }
}

2- CategoriaRepository

using Microsoft.EntityFrameworkCore;
using ProdutosApi.Models;

namespace ProdutosApi.Repositories.Implementations;

public class CategoriaRepository : BaseRepository<Categoria>, ICategoriaRepository
{
    public CategoriaRepository(AppDbContext dbContext) : base(dbContext)
    { }

    public async Task<IEnumerable<Categoria>> GetAllCategoriasAsync()
    {
        return await FindAll()
                     .OrderBy(cat => cat.Nome)
                     .ToListAsync();
    }
    public async Task<Categoria> GetCategoriaByIdAsync(int id)
    {
        return await FindByCondition(cat => cat.CategoriaId.Equals(id))
            .FirstOrDefaultAsync();
    }
    public async Task<Categoria> GetCategoriaWithDetailsAsync(int id)
    {
        return await FindByCondition(cat => cat.CategoriaId.Equals(id))
                    .Include(ac => ac.Produtos)
                    .FirstOrDefaultAsync();
    }
    public void CreateCategoria(Categoria categoria)
    {
        Create(categoria);
    }
    public void UpdateCategoria(Categoria categoria)
    {
        Update(categoria);
    }
    public void DeleteCategoria(Categoria categoria)
    {
        Delete(categoria);
    }
}

3- ProdutoRepository

using Microsoft.EntityFrameworkCore;
using ProdutosApi.Models;

namespace ProdutosApi.Repositories.Implementations;

public class ProdutoRepository : BaseRepository<Produto>, IProdutoRepository
{
    public ProdutoRepository(AppDbContext dbContext) : base(dbContext)
    { }

    public async Task<IEnumerable<Produto>> GetAllProdutosAsync()
    {
        return await FindAll()
                     .OrderBy(prod => prod.Nome)
                     .ToListAsync();
    }
    public async Task<Produto> GetProdutoByIdAsync(int id)
    {
        return await FindByCondition(prod => prod.ID.Equals(id))
            .FirstOrDefaultAsync();
    }
    public void CreateProduto(Produto produto)
    {
        Create(produto);
    }
    public void UpdateProduto(Produto produto)
    {
        Update(produto);
    }
    public void DeleteProduto(Produto produto)
    {
        Delete(produto);
    }
    public Task<bool> IsProdutoAtivo(string nome)
    {
        var resultado = _dbContext.Produtos.Any(e => e.Nome.Equals(nome));
        return Task.FromResult(resultado);
    }
}

 

4- RepositoryWrapper

using ProdutosApi.Models;

namespace ProdutosApi.Repositories.Implementations
{
    public class RepositoryWrapper : IRepositoryWrapper
    {
        private AppDbContext _repoContext;
        private ICategoriaRepository _categoria;
        private IProdutoRepository _produto;
        public ICategoriaRepository CategoriaRepo
        {
            get
            {
                if (_categoria == null)
                {
                    _categoria = new CategoriaRepository(_repoContext);
                }
                return _categoria;
            }
        }

        public IProdutoRepository ProdutoRepo
        {
            get
            {
                if (_produto == null)
                {
                    _produto = new ProdutoRepository(_repoContext);
                }
                return _produto;
            }
        }

        public RepositoryWrapper(AppDbContext repositoryContext)
        {
            _repoContext = repositoryContext;
        }

        public async Task SaveAsync()
        {
            await _repoContext.SaveChangesAsync();
        }
    }
}

Agora podemos implementar os Controllers. Eu vou mostrar a implementação do controlador ProdutosController  e vou deixar a seu cargo implementar o controlador para Categorias.

1- ProdutosController

using Microsoft.AspNetCore.Mvc;
using ProdutosApi.Models;
using ProdutosApi.Repositories;

namespace ProdutosApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProdutosController : ControllerBase
    {
        private readonly ILogger<ProdutosController> _logger;
        private readonly IRepositoryWrapper _repoContext;

        public ProdutosController(ILogger<ProdutosController> logger,
            IRepositoryWrapper context)
        {
            _logger = logger;
            _repoContext = context;
        }

        [HttpGet]
        public async Task<IActionResult> GetProdutos()
        {
            try
            {
                var produtos = await _repoContext.ProdutoRepo.GetAllProdutosAsync();
                _logger.LogInformation($"Retornou todos os produtos do banco");
                return Ok(produtos);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Erro ao processar GetProdutos :  {ex.Message}");
                return StatusCode(500, "Internal server error");
            }
        }

        [HttpGet("{id}", Name = "ProdutoById")]
        public async Task<ActionResult<Produto>> GetProdutoById(int id)
        {
            var produto = await _repoContext.ProdutoRepo.GetProdutoByIdAsync(id);

            if (produto == null)
            {
                _logger.LogError($"Produto com id: {id}, não encontrado no banco.");
                return NotFound();
            }
            else
            {
                _logger.LogInformation($"Retornado produto com id: {id}");
                return Ok(produto);
            }
        }

        [HttpPost]
        public async Task<IActionResult> CreateProduto([FromBody] Produto produto)
        {
            try
            {
                if (produto == null)
                {
                    _logger.LogError("Objeto produto enviado ao cliente é nulo.");
                    return BadRequest("Objeto Produto é null");
                }

                if (!ModelState.IsValid)
                {
                    _logger.LogError("Objeto produto inválido");
                    return BadRequest("Objeto produto inválido");
                }

                _repoContext.ProdutoRepo.CreateProduto(produto);
                await _repoContext.SaveAsync();

                return CreatedAtRoute("ProdutoById", new { id = produto.ID }, produto);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Erro ao processar CreateProduto : {ex.Message}");
                return StatusCode(500, "Internal server error");
            }
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateProduto(int id, [FromBody] Produto produto)
        {
            try
            {
                if (produto == null)
                {
                    _logger.LogError("Objeto produto enviado ao cliente é nulo.");
                    return BadRequest("Objeto Produto é null");
                }

                if (!ModelState.IsValid)
                {
                    _logger.LogError("Objeto produto inválido");
                    return BadRequest("Objeto produto inválido");
                }

                var produtoEntity = await _repoContext.ProdutoRepo.GetProdutoByIdAsync(id);
                if (produtoEntity == null)
                {
                    _logger.LogError($"Produto com id: {id}, não encontrado no banco.");
                    return NotFound();
                }

                _repoContext.ProdutoRepo.UpdateProduto(produtoEntity);
                await _repoContext.SaveAsync();

                return NoContent();
            }
            catch (Exception ex)
            {
                _logger.LogError($"Erro ao processar UpdateProduto: {ex.Message}");
                return StatusCode(500, "Internal server error");
            }
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteProduto(int id)
        {
            try
            {
                var produto = await _repoContext.ProdutoRepo.GetProdutoByIdAsync(id);
                if (produto == null)
                {
                    _logger.LogError($"Produto com id: {id}, não encontrado.");
                    return NotFound();
                }

                _repoContext.ProdutoRepo.DeleteProduto(produto);
                await _repoContext.SaveAsync();

                return NoContent();
            }
            catch (Exception ex)
            {
                _logger.LogError($"Erro ao processar DeleteProduto: {ex.Message}");
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

Para que tudo funcione corretamente temos que registrar os serviços no arquivo Program:

using Microsoft.EntityFrameworkCore;
using ProdutosApi.Models;
using ProdutosApi.Repositories;
using ProdutosApi.Repositories.Implementations;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var conexaoDB = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(conexaoDB);
});

builder.Services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Executando o projeto teremos o resultado a seguir:

Pegue o projeto completo aqui : ProdutosApi_RepoAsync.zip

E estamos conversados...

"Portanto, lembrai-vos de que vós noutro tempo éreis gentios na carne, e chamados incircuncisão pelos que na carne se chamam circuncisão feita pela mão dos homens;
Que naquele tempo estáveis sem Cristo, separados da comunidade de Israel, e estranhos às alianças da promessa, não tendo esperança, e sem Deus no mundo."
Efésios 2:11,12

Referências:


José Carlos Macoratti