.NET

19 abr, 2018

ASP.NET Core 2.0: CRUD em API utilizando JWT, EF Core InMemory e Docker

Publicidade

Neste artigo, trago um exemplo de implementação de um CRUD em uma API REST baseada no ASP.NET Core 2.0. O projeto em questão é muito simples, permitindo a inclusão, alteração, exclusão e consulta a dados de produtos (descrição + códigos de barras).

Além do .NET Core 2.0/ASP.NET Core 2.0, foram utilizadas na construção desta aplicação as seguintes tecnologias:

  • Entity Framework Core InMemory, que permitirá o uso das funcionalidades típicas do Entity Framework com armazenamento em memória, dispensando a necessidade de criação física de uma base de dados relacional;
  • JWT (JSON Web Tokens) com a utilização de tokens para o acesso seguro à API;
  • Docker, para a criação de uma imagem pública da API de produtos e disponibilização da mesma para uso geral.

O código do fonte do projeto está no seguinte repositório do GitHub:

Um vídeo descrevendo a estrutura utilizada para a API foi publicado também no canal Coding Night:

Nas próximas seções você poderá encontrar maiores detalhes e referências úteis sobre os recursos empregados na API REST de produtos.

Utilizando o Entity Framework Core InMemory

Para habilitar o uso da versão em memória do Entity Framework Core foi adicionado ao projeto o package Microsoft.EntityFrameworkCore.InMemory:

No caso específico da API de produtos duas entidades serão consideradas: Produto e Usuario.

A classe Produto conterá o código de barras, a descrição/nome de um item e seu respectivo preço:

namespace APIProdutos.Models
{
    public class Produto
    {
        private string _codigoBarras;
        public string CodigoBarras
        {
            get => _codigoBarras;
            set => _codigoBarras = value?.Trim().ToUpper();
        }

        private string _nome;
        public string Nome
        {
            get => _nome;
            set => _nome = value?.Trim();
        }

        public double Preco { get; set; }
    }
}

Já na classe Usuario, estão o ID do mesmo e sua chave de acesso:

A classe de contexto ApplicationDbContext referencia as entidades Produto e Usuario. Embora a API aqui descrita faça uso do Entity Framework Core InMemory, este tipo (ApplicationDbContext) em nada difere de outra implementação que empregue uma base relacional:

A classe ProdutoService fará uso de ApplicationDbContext, contendo as diversas operações de CRUD relativas ao cadastro de produtos:

O tipo ProdutoService também referencia a classe Resultado, na qual estarão informações indicando o sucesso ou não de uma operação de CRUD:

using System.Collections.Generic;

namespace APIProdutos
{
    public class Resultado
    {
        public string Acao { get; set; }

        public bool Sucesso
        {
            get { return _Inconsistencias == null || Inconsistencias.Count == 0; }
        }

        private List<string> _Inconsistencias = new List<string>();
        public List<string> Inconsistencias
        {
            get { return _Inconsistencias; }
        }
    }
}

Já a classe UsuarioService será utilizada na validação de credenciais de um usuário, além de permitir a inclusão de tais dados:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using APIProdutos.Data;
using APIProdutos.Models;

namespace APIProdutos.Business
{
    public class UsuarioService
    {
        private ApplicationDbContext _context;

        public UsuarioService(ApplicationDbContext context)
        {
            _context = context;
        }

        public Usuario Obter(string id)
        {
            return _context.Usuarios.Where(
                u => u.ID == id).FirstOrDefault();
        }

        public void Incluir(Usuario dadosUsuario)
        {
            _context.Usuarios.Add(dadosUsuario);
            _context.SaveChanges();
        }
    }

As dependências com os tipos ApplicationDbContext, ProdutoService e UsuarioService serão configuradas através do método ConfigureServices da classe Startup:

  • Quanto a ApplicationDbContext, nota-se uma chamada ao método UseInMemoryDatabase a fim de possibilitar o armazenamento de dados em memória a partir do Entity Framework Core;
  • As chamadas ao método AddScoped farão com que dependências de ProdutoService e UsuarioService utilizem uma única instância para cada um desses tipos durante o processamento de uma solicitação HTTP;
  • Já o método Configure receberá como parâmetro uma instância de UsuarioService (resolvida via injeção de dependências), com este objeto sendo empregado no cadastramento de 2 credenciais de acesso à API (via operação Incluir).
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore;
using APIProdutos.Data;
using APIProdutos.Models;
using APIProdutos.Business;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;

namespace APIProdutos
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseInMemoryDatabase("InMemoryDatabase"));
            services.AddScoped<ProdutoService>();
            services.AddScoped<UsuarioService>();

            // Outras implementações envolvendo o uso de JWT...
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
            UsuarioService usrService)
        {
            usrService.Incluir(
                new Usuario() { ID = "usuario01", ChaveAcesso = "94be650011cf412ca906fc335f615cdc" });
            usrService.Incluir(
                new Usuario() { ID = "usuario02", ChaveAcesso = "531fd5b19d58438da0fd9afface43b3c" });

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();

Caso queira saber mais sobre injeção de dependências no ASP.NET Core, acesse o vídeo a seguir do Canal .NET:

Habilitando o uso de JWT

Os ajustes necessários para habilitar o uso de JWT (JSON Web Tokens) envolvem alterações na classe Startup, além da implementação de um Controller chamado LoginController. Já abordei em detalhes este processo no seguinte artigo:

Por questões de simplificação, não apresentarei tais instruções neste artigo, até porque o projeto/repositório indicado no início do artigo já contém todas as modificações esperadas para viabilizar o uso de tokens.

Os próximos artigos contêm referências adicionais sobre o uso de JWT com o .NET Core:

Implementando o Controller para CRUD

A classe ProdutosController será responsável pelas operações de CRUD envolvendo o cadastro de produtos:

  • Este tipo foi marcado com o atributo Authorize, no qual foi indicado ainda o uso de Bearer Authentication (autenticação via tokens);
  • As diversas operações de CRUD foram implementadas através das Actions Get, Post, Put e Delete, com as mesmas acionando métodos definidos na classe ProdutoService.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using APIProdutos.Business;
using APIProdutos.Models;
using Microsoft.AspNetCore.Authorization;

namespace APIProdutos.Controllers
{
    [Authorize("Bearer")]
    [Route("api/[controller]")]
    public class ProdutosController : Controller
    {
        private ProdutoService _service;

        public ProdutosController(ProdutoService service)
        {
            _service = service;
        }

        [HttpGet]
        public IEnumerable<Produto> Get()
        {
            return _service.ListarTodos();
        }

        [HttpGet("{codigoBarras}")]
        public IActionResult Get(string codigoBarras)
        {
            var produto = _service.Obter(codigoBarras);
            if (produto != null)
                return new ObjectResult(produto);
            else
                return NotFound();
        }

        [HttpPost]
        public Resultado Post([FromBody]Produto produto)
        {
            return _service.Incluir(produto);
        }

        [HttpPut]
        public Resultado Put([FromBody]Produto produto)
        {
            return _service.Atualizar(produto);
        }

        [HttpDelete("{codigoBarras}")]
        public Resultado Delete(string codigoBarras)
        {
            return _service.Excluir(codigoBarras);
        }
    }
}

Gerando e publicando uma imagem Docker

Os procedimentos necessários para a publicação da imagem pública desta API na Docker Store/Docker Hub são os mesmos descritos no seguinte artigo:

Tal imagem poderá ser utilizada por Single-page Applications (SPAs) ou outros tipos de aplicações sem a preocupação com ferramentas necessárias para a execução da API descrita neste artigo. O único pré-requisito neste caso é a instalação prévia do Docker.

Importante ressaltar ainda que, por envolver o uso de REST, qualquer solução que suporte este padrão poderá interagir com a API, independentemente da tecnologia empregada em sua construção.

A imagem da API de produtos já se encontra no Docker Hub/Docker Store e está identificada como renatogroffe/apiprodutos:

Execução e testes da API a partir de um container Docker

A imagem renatogroffe/apiprodutos poderá ser baixada via PowerShell através do comando docker pull renatogroffe/apiprodutos:

Já para a criação de um container na porta 12345, será executado o comando:

docker run -p 12345:80 — name testeapiprodutos -d renatogroffe/apiprodutos

O comando docker ps mostrará que o container em questão já está ativo e executando na porta 12345:

As credenciais de usuários definidas via código (mais precisamente no método Configure da classe Startup) foram as seguintes:

  • ID = usuario01, ChaveAcesso = 94be650011cf412ca906fc335f615cdc
  • ID = usuario02, ChaveAcesso = 531fd5b19d58438da0fd9afface43b3c

Na imagem a seguir, é possível observar a obtenção de um token (válido por 1 hora) via Postman e empregando uma destas credenciais:

Uma requisição do tipo POST e direcionada a ProdutosController será então gerada, tendo por objetivo cadastrar um primeiro produto:

  • O token obtido no início dos testes será utilizado para o acesso à API de produtos;
  • No corpo da solicitação estarão os dados do produto a ser armazenado em memória, com a API devolvendo um conjunto de dados indicando o sucesso nesta operação.

Referências