ASP.NET Core 3.1 - CRUD : Web API com EF Core e Tokens JWT


Hoje teremos um roteiro básico para criar Web APIs para realizar o CRUD com ASP .NET Core 3.1 e EF Core usando Tokens JWT.

Continuando a primeira parte do artigo vamos iniciar agora a criação da nossa Web API que será representada pelo controlador ProdutosController.

Antes de iniciar a criação da Web API temos que definir o escopo de atuação da API e  decidir se iremos usar ou não  um repositório e se esse repositório será genérico ou não.

Nossa Web API vai gerenciar informações de produtos, assim vamos traçar alguns objetivos que nossa API deverá cumprir:

- Vamos criar um serviço RESTful que permita que aplicativos clientes gerenciem um catálogo de produtos;
- Ela precisa expor endpoints para criar, ler, editar e excluir produtos;
- Para produtos, precisamos armazenar o código, nome, preço, estoque e a imagem do produto;

Para desenvolver essa API, basicamente precisamos de apenas um endpoint na API para gerenciar produtos :

Um endpoint de uma API é a URL onde seu serviço pode ser acessado por uma aplicação cliente.

Uma API é um conjunto de rotinas, protocolos e ferramentas para construir aplicações.

Em termos de comunicação JSON, podemos pensar nas respostas da seguinte forma:

1- Produtos

API endpoint :  /api/produtos

JSON Response (para requisições GET)

{
  [
    {
      “produtoid”: 1,
      “nome”: “Caderno”,
      “preco”: 6,99,
      “Estoque": 50,
      "Imagem":"caderno.jpg"
    },
    … // Outros produtos
  ]
}

Usar ou não usar um Repositório eis a questão

O padrão de projeto Repository acrescenta uma camada de abstração no topo da camada de consultas e ajuda  eliminar lógica duplicada na implementação do código de suas consultas ao modelo de entidades.

Foi Martin Fowler definiu o padrão Repository no seu livro - Patterns of Enterprise Application Architecture - da seguinte forma: "Intermedeia entre o domínio e as camadas de mapeamento de dados usando uma interface de coleção para acessar objetos de domínio." (numa tradução livre by Macoratti)

Assim, um repositório é essencialmente uma coleção de objetos de domínio em memória, e, com base nisso o padrão Repository permite realizar o isolamento entre a camada de acesso a dados (DAL) de sua aplicação e sua camada de apresentação (UI) e camada de negócios (BLL).

Ao utilizar o padrão Repository você pode realizar a persistência e a separação de responsabilidades em seu código de acesso a dados visto que ele encapsula a lógica necessária para persistir os objetos do seu domínio na sua fonte de armazenamento de dados.

Dessa forma o repositório é um objeto que encapsula a camada de acesso a dados e contém a lógica para retornar e mapear os dados para um modelo de entidades.

A alternativa ao repositório seria criar a Web API acessando diretamente o Entity Framework Core, e, como o EF Core implementa o padrão repositório, muitos consideram que criar um repositório seria desnecessário neste contexto.

Na verdade, dependendo do cenário, realmente não precisaríamos criar um repositório, mas, se por algum motivo você precisar mudar a camada de acesso aos dados, trocando o EF Core por outro ORM, ou usar outra abordagem, vai ter um trabalho maior do que se tivesse usado um repositório.

Outro detalhe é criar um repositório genérico ou não ?

Neste caso vai contar a complexidade de seu contexto e como um repositório genérico permite reutilizar código vamos adotar essa abordagem.

Assim vamos criar um repositório genérico que podemos esquematizar de forma bem simples na figura a seguir:

A abordagem tradicional para criar um repositório é definir uma interface e a seguir a sua implementação.

Criando um repositório genérico

Em nosso projeto vamos criar a pasta Repositories e nesta pasta criar a interface IGenericRepository e a classe concreta GenericRepository que implementa a interface.

1- Interface IGenericRepository

using System.Collections.Generic;
namespace InventarioNET.Repositories
{
    public interface IGenericRepository<T> where T : class
    {
        Task<IEnumerable<T>> GetAll();
        Task<T> GetById(int id);
        Task Insert(T obj);
        Task Update(int id, T obj);
        Task Delete(int id);
    }
}

Na assinatura da classe estamos declarando : 

public interface IGenericRepository<T> where T : class  - aqui T é uma classe;

-
Task<IEnumerable<T>> GetAll() - Este método retorna os dados como IEnumerable;
- Task<T> GetById(int id) - Retorna um objeto do tipo T pelo seu id;
- Task Add(T obj) - Recebe o objeto T para realizar a inclusão no banco de dados;
- Task Update(int id, T obj) - Recebe o id e o objeto  T para realizar a atualização no banco de dados;
- Task Delete(int id) - Recebe o objeto T e realiza a exclusão no banco de dados;

1- classe concreta GenericRepository

using InventarioNET.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace InventarioNET.Repositories
{
    public class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        private AppDbContext context = null;
        public GenericRepository(AppDbContext _context)
        {
            this.context = _context;
        }

        public async Task<IEnumerable<T>> GetAll()
        {
            return await context.Set<T>().AsNoTracking().ToListAsync();
        }
        public async Task<T> GetById(int id)
        {
            return await context.Set<T>().FindAsync(id);
        }
        public async Task Insert(T obj)
        {
            await context.Set<T>().AddAsync(obj);
            await context.SaveChangesAsync();
        }
        public async Task Update(int id, T obj)
        {
            context.Set<T>().Update(obj);
            await context.SaveChangesAsync();
        }
        public async Task Delete(int id)
        {
            var entity = await GetById(id);
            context.Set<T>().Remove(entity);
            await context.SaveChangesAsync();
        }
    }
}

Aqui injetamos a instância do nosso contexto no construtor da API. Observe que não temos nenhum comando SQL, nenhuma declaração de objetos ADO .NET como connection, command, dataset, datareader, etc.

O método Update recebe uma entidade e define o seu EntityState como Modified informando ao contexto que a entidade foi alterada e usando o método Update() para atualizar a entidade.

O contexto não só trata a referência para todos os objetos recuperados do banco de dados, mas também detém os estados da entidade e mantém as modificações feitas nas propriedades da entidade. Este recurso é conhecido como controle de alterações ou ChangeTracking.

Temos assim um repositório genérico assíncrono bem simples que poderemos usar para realizar o acesso e as consultas para qualquer entidade em nosso projeto.

Agora temos que definir a implementação do repositório para os nosso produtos e para isso vamos criar a interface IProdutoRepository e ProdutoRepository:

1- IProdutoRepository

using InventarioNET.Models;
namespace InventarioNET.Repositories
{
    public interface IProdutoRepository : IGenericRepository<Produto>
    {
    }
}

Note que nossa interface herda a interface IGenericRepository<Produto> e terá acesso a todo o seu contrato. Aqui poderíamos ter definido algum método específico nesta interface.

2- ProdutoRepository

using InventarioNET.Models;
namespace InventarioNET.Repositories
{
    public class ProdutoRepository : GenericRepository<Produto>, IProdutoRepository
    {
        public ProdutoRepository(AppDbContext repositoryContext)
             : base(repositoryContext)
        {
        }
    }
}

Para poder usar o repositório como um serviço vamos registrá-lo na classe Startup no método ConfigureServices:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContextPool<AppDbContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });
             services.AddScoped<IProdutoRepository, ProdutoRepository>();

            services.AddControllers();
        }

O método ConfigureServices() é responsável por definir os serviços que a aplicação vai usar, incluindo recursos da plataforma como ASP .NET Core MVC e Entity Framework.

Na implementação da Injeção de dependência do ASP.NET Core, vemos o conceito de lifetimes ou "tempo de vidas". Um tempo de vida especifica quando um objeto injetado é criado ou recriado. Existem três possibilidades:

  1. - Transient : Criado a cada vez que são solicitados.
  2. - Scoped: Criado uma vez por solicitação.
  3. - Singleton: Criado na primeira vez que são solicitados. Cada solicitação subseqüente usa a instância que foi criada na primeira vez.

O parâmetro IServiceCollection permite configurar diferentes tipos de serviços seja por criação de objeto ou correspondência a uma interface específica e suporta os lifetimes mencionados.

No exemplo estamos adicionando um serviço Scoped.

Criando o controlador ProdutosController

Agora podemos criar a nossa Web API representada pelo controlador ProdutosController que vai usar o repositório que acabamos de criar.

Selecione a pasta Controllers e no menu Project clique em Add-> New Item;

Na janela informe o nome ProdutosController e clique em Add;

A seguir inclua o código abaixo no controlador ProdutosController:

using InventarioNET.Models;
using InventarioNET.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace InventarioNET.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProdutosController : Controller
    {
        private readonly IProdutoRepository repository;
        public ProdutosController(IProdutoRepository _context)
        {
            repository = _context;
        }
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Produto>>> GetProdutos()
        {
            var produtos = await repository.GetAll();
            if (produtos == null)
            {
                return BadRequest();
            }
            return Ok(produtos.ToList());
        }
        // GET: api/Products/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Produto>> GetProduto(int id)
        {
            var produto = await repository.GetById(id);
            if (produto == null)
            {
                return NotFound("Produto não encontrado pelo id informado");
            }
            return Ok(produto);
        }
        // POST api/<controller>  
        [HttpPost]
        public async Task<IActionResult> PostProduto([FromBody]Produto produto)
        {
            if (produto == null)
            {
                return BadRequest("Produto é null");
            }            
            await repository.Insert(produto);
            return CreatedAtAction(nameof(GetProduto), new { Id = produto.ProdutoId }, produto);
        }
        [HttpPut("{id}")]
        public async Task<IActionResult> PutProduto(int id, Produto produto)
        {
            if (id != produto.ProdutoId)
            {
                return BadRequest($"O código do produto {id} não confere");
            }
            try
            {
                await repository.Update(id, produto);
            }
            catch (DbUpdateConcurrencyException)
            {
                throw;
            }
            return Ok("Atualização do produto realizada com sucesso");
        }
        [HttpDelete("{id}")]
        public async Task<ActionResult<Produto>> DeleteProduto(int id)
        {
            var produto = await repository.GetById(id);
            if (produto == null)
            {
                return NotFound($"Produto de {id} foi não encontrado");
            }
            await repository.Delete(id);
            return Ok(produto);
        }
    }
}

Observe que injetamos o serviço do nosso repositório no construtor da API.

No método POST estamos retornando um código de status 201 gerado pelo método CreatedAtAction quando um produto for criado.

Nota: Uma alternativa para chamar CreatedAtAction é retornar new CreatedAtActionResult (nameof (GetProduto), "Produtos", novo {id = produt.ProdutoId}, produto) ;.

Temos assim a nossa API pronta usando o repositório genérico criado e agora podemos consumir esta API.

O último detalhe que vamos alterar é alterar o arquivo launchSettings.json da pasta Properties do projeto e definir o valor de launchUrl para "api/produtos".

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:4314",
      "sslPort": 44344
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/produtos",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "InventarioNET": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "weatherforecast",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Agora essa será a url que o navegador vai abrir quando iniciar a aplicação.

Na próxima parte do artigo vamos testar a nossa Web API REST usando o Postman.

"E se abrires a tua alma ao faminto, e fartares a alma aflita; então a tua luz nascerá nas trevas, e a tua escuridão será como o meio-dia."
Isaías 58:10

Referências:


José Carlos Macoratti