Artigo original: An awesome guide on how to build RESTful APIs with ASP.NET Core

Este artigo vai servir como um guia passo a passo sobre como implementar APIs RESTful limpas e de fácil manutenção.

Visão geral

RESTful não é um termo novo. Refere-se a um estilo de arquitetura em que os serviços da web recebem e enviam dados de e para aplicações de client. O objetivo dessas aplicações é centralizar os dados que as diversas aplicações de client usarão.

Escolher as ferramentas certas para escrever serviços RESTful é crucial, pois precisamos nos preocupar com escalabilidade, manutenção, documentação e todos os outros aspectos relevantes. O ASP.NET Core nos oferece uma API poderosa e fácil de usar, que é ótima para atingir esses objetivos.

Neste artigo, mostrarei como escrever uma API RESTful bem estruturada para um cenário "quase" igual ao do mundo real, usando o framework ASP.NET Core. Vou detalhar padrões e estratégias comuns para simplificar o processo de desenvolvimento.

Também mostrarei como integrar estruturas e bibliotecas comuns, como Entity Framework Core e AutoMapper, para fornecer as funcionalidades necessárias.

Pré-requisitos

Espero que você tenha conhecimento dos conceitos de programação orientada a objetos.

Mesmo abordando muitos detalhes da linguagem de programação C#, recomendo que você tenha conhecimentos básicos sobre esse assunto.

Também suponho que você saiba o que é REST, como funciona o protocolo HTTP, o que são endpoints de API e o que é JSON. Aqui está um ótimo tutorial introdutório sobre o assunto (texto em inglês). O requisito final é que você entenda como funcionam os bancos de dados relacionais.

Para codificar comigo, você terá que instalar o .NET Core 2.2, assim como o Postman, a ferramenta que vou usar para testar a API. Eu recomendo que você use um editor de código como o Visual Studio Code para desenvolver a API. Escolha o editor de código de sua preferência. Se você escolher este editor de código, recomendo que instale a extensão de C# para ter um melhor realce de código.

Nota do tradutor: a versão do .NET Core 2.2 mencionada neste texto, de acordo com a Microsoft, já atingiu seu final de ciclo no momento desta tradução. A empresa recomenda utilizar o .NET 6.0 – que, acreditamos, não gerará alterações sensíveis ao resultado.

Você pode encontrar um link para o repositório da API no GitHub ao final deste artigo, para conferir o resultado final.

O escopo

Vamos escrever uma API web fictícia para um supermercado. Vamos imaginar que temos que implementar o seguinte escopo:

  • Criar um serviço RESTful que permita que aplicações de client gerenciem o catálogo de produtos do supermercado. Ele precisa expor endpoints para criar, ler, editar e excluir categorias de produtos, como laticínios e cosméticos, além de gerenciar produtos dessas categorias.
  • Para as categorias, precisamos armazenar seus nomes. Para os produtos, precisamos armazenar seus nomes, unidade de medida (por exemplo, kg para produtos medidos por peso), quantidade na embalagem (por exemplo, 10 se o produto for um pacote de biscoitos) e suas respectivas categorias.

Para simplificar o exemplo, não tratarei de produtos em estoque, envio de produtos, segurança e qualquer outra funcionalidade. O escopo fornecido é suficiente para mostrar como o ASP.NET Core funciona.

Para desenvolver esse serviço, precisamos basicamente de dois endpoints de API: um para gerenciar categorias e outro para gerenciar produtos. Em termos de comunicação por meio de JSON, podemos pensar nas respostas da seguinte forma:

Endpoint da API: /api/categories

Resposta em JSON (para solicitações GET):

{
  [
    { "id": 1, "name": "Fruits and Vegetables" },
    { "id": 2, "name": "Breads" },
    … // Outras categorias
  ]
}

Endpoint da API: /api/products

Resposta em JSON (para solicitações GET):

{
  [
    {
      "id": 1,
      "name": "Sugar",
      "quantityInPackage": 1,
      "unitOfMeasurement": "KG"
      "category": {
        "id": 3,
        "name": "Sugar"
      }
    },
    … // Outros produtos
  ]
}

Vamos começar a escrever a aplicação.

Etapa 1 — Criando a API

Em primeiro lugar, temos que criar a estrutura de pastas para o serviço da web e, em seguida, usar as ferramentas de CLI do .NET para estruturar uma API web básica. Abra o terminal ou prompt de comando (dependendo do sistema operacional que você estiver usando) e digite os seguintes comandos, em sequência:

mkdir src/Supermarket.API

cd src/Supermarket.API

dotnet new webapi

Os dois primeiros comandos simplesmente criam um novo diretório para a API e alteram o local atual para a nova pasta. O último gera um novo projeto seguindo o modelo da API web, que é o tipo de aplicação que estamos desenvolvendo. Você pode ler mais sobre esses comandos e outros modelos de projeto que você pode gerar conferindo este link.

O novo diretório agora terá a seguinte estrutura:

Visão geral da estrutura

Uma aplicação ASP.NET Core consiste em um grupo de middlewares (pequenos pedaços da aplicação anexados ao pipeline da aplicação, que manipulam solicitações e respostas) configurados na classe Startup. Se você já trabalhou com frameworks como Express.js antes, esse conceito não é novo para você.

Quando a aplicação é iniciada, o método Main, da classe Program, é chamado. Ele cria um host padrão usando a configuração de inicialização, expondo a aplicação via HTTP através de uma porta específica (por padrão, a porta 5000 para HTTP e a porta 5001 para HTTPS).

Dê uma olhada na classe ValuesController, dentro da pasta Controllers. Ela expõe métodos que serão chamados quando a API receber requisições através da rota /api/values.

Não se preocupe se você não entender alguma parte desse código. Vou detalhar cada um ao desenvolver os endpoints de API necessários. Por enquanto, basta excluir esta classe, pois não vamos usá-la.

Etapa 2 — Criando os modelos de domínio

Vou aplicar alguns conceitos de design que manterão a aplicação simples e fácil de manter.

Escrever código que possa ser entendido e mantido por você mesmo não é tão difícil, mas você deve ter em mente que trabalhará em equipe. Se você não tomar cuidado com a forma como escreve seu código, o resultado será um monstro que dará a você e seus colegas de equipe constantes dores de cabeça. Parece extremo, certo? Mas acredite, essa é a verdade.

0_Obq8C7c3EuzmJBZb
wtf — code quality measurement, criado por smitty42 e sob a licença CC-BY-ND 2.0

Vamos começar escrevendo a camada de domínio. Essa camada terá nossas classes de modelos, as classes que representarão nossos produtos e categorias, além de repositórios e interfaces de serviços. Vou explicar esses dois últimos conceitos daqui a pouco.

Dentro do diretório Supermarket.API, crie uma nova pasta chamada Domain. Dentro da nova pasta de domínio, crie outra chamada Models. O primeiro modelo que temos que adicionar a essa pasta é o Category. Inicialmente, será uma classe simples de Plain Old CLR Object (POCO) – (que pode ser traduzido como "Objeto de CLR puro e simples"). Isso significa que a classe terá apenas propriedades para descrever suas informações básicas.

A classe tem uma propriedade Id, para identificar a categoria, e uma propriedade Name. Também temos uma propriedade Products. Essa última será usada pelo Entity Framework Core, o ORM que a maioria das aplicações ASP.NET Core usa para persistir dados em um banco de dados, para mapear o relacionamento entre categorias e produtos. Também faz sentido pensar em termos de programação orientada a objetos, já que uma categoria tem muitos produtos relacionados.

Também temos que criar o modelo do produto. Na mesma pasta, adicione uma nova classe Product.

O produto também possui propriedades para o Id e o Name. Há também uma propriedade QuantityInPackage, que informa quantas unidades do produto temos em um pacote (lembre-se do exemplo dos biscoitos, do escopo da aplicação) e uma propriedade UnitOfMeasurement. Esta é representada por um tipo enum, que representa uma enumeração de possíveis unidades de medida. As duas últimas propriedades, CategoryId e Category, serão usadas pelo ORM para mapear o relacionamento entre produtos e categorias. Isso indica que um produto tem uma, e apenas uma, categoria.

Vamos definir a última parte de nossos modelos de domínio, o enum EUnitOfMeasurement.

Por convenção, enums não precisam começar com um "E" na frente de seus nomes, mas em algumas bibliotecas e frameworks você encontrará esse prefixo como uma forma de distinguir enums de interfaces e classes.

O código é realmente simples. Aqui, definimos apenas um punhado de possibilidades de unidades de medida. No entanto, em um sistema real de supermercado, você pode ter muitas outras unidades de medida e talvez um modelo separado para isso.

Observe o atributo Description aplicado a todas as possibilidades de enumeração. Um atributo é uma maneira de definir metadados sobre classes, interfaces, propriedades e outros componentes da linguagem C#. Nesse caso, vamos usá-lo para simplificar as respostas do endpoint da API do produto, mas você não precisa se preocupar com isso por enquanto. Voltaremos aqui mais tarde.

Nossos modelos básicos estão prontos para serem usados. Agora podemos começar a escrever o endpoint da API que vai gerenciar todas as categorias.

Etapa 3 — A API de categorias

Na pasta Controllers, adicione uma nova classe chamada CategoriesController.

Por convenção, todas as classes nessa pasta que terminam com o sufixo "Controller" se tornarão controllers (controladores) da nossa aplicação. Isso significa que eles vão lidar com solicitações e respostas. Você precisa herdar essa classe da classe Controller, definida no namespace Microsoft.AspNetCore.Mvc.

Um namespace consiste em um grupo de classes, interfaces, enumerações e estruturas relacionadas. Você pode pensar nisso como algo semelhante a módulos da linguagem Javascript, ou pacotes de Java (links em inglês).

O novo controlador deve responder através da rota /api/categories. Conseguimos isso adicionando o atributo Route acima do nome da classe, especificando um espaço reservado que indica que a rota deve usar o nome da classe sem o sufixo do controlador, por convenção.

Vamos começar a lidar com solicitações GET. Em primeiro lugar, quando alguém solicita dados de /api/categories via verbo GET, a API precisa retornar todas as categorias. Podemos criar um serviço de categoria para este fim.

Conceitualmente, um serviço é basicamente uma classe ou interface que define métodos para lidar com alguma lógica de negócios. É uma prática comum em muitas linguagens de programação diferentes criar serviços para lidar com a lógica de negócios, como autenticação e autorização (texto em inglês), pagamentos, fluxos de dados complexos, cache e tarefas que exigem alguma interação entre outros serviços ou modelos.

Usando serviços, podemos isolar o tratamento de solicitações e respostas da lógica real necessária para concluir as tarefas.

O serviço que vamos criar inicialmente definirá um único comportamento, ou método: um método de listagem. Esperamos que este método retorne todas as categorias existentes no banco de dados.

Para simplificar, não lidaremos com paginação ou filtragem de dados nesse caso. Escreverei um artigo no futuro mostrando como lidar facilmente com esses recursos.

Para definir um comportamento esperado para algo em C# (e em outras linguagens orientadas a objetos, como Java, por exemplo), definimos uma interface. Uma interface diz como algo deve funcionar, mas não implementa a lógica real para o comportamento. A lógica é implementada em classes que implementam a interface. Se esse conceito não estiver claro para você, não se preocupe. Você vai entender daqui a pouco.

Dentro da pasta Domain, crie um novo diretório chamado Services. Lá, adicione uma interface chamada ICategoryService. Por convenção, todas as interfaces devem começar com a letra maiúscula “I” em C#. Defina o código da interface da seguinte forma:

As implementações do método ListAsync devem retornar de forma assíncrona uma enumeração de categorias.

A classe Task, encapsulando o retorno, indica assincronia. Precisamos pensar em um método assíncrono devido ao fato de termos que esperar o banco de dados concluir alguma operação para retornar os dados. Esse processo pode demorar um pouco. Observe também o sufixo "async". É uma convenção que indica que nosso método deve ser executado de forma assíncrona.

Temos muitas convenções, certo? Eu pessoalmente gosto, porque mantém as aplicações fáceis de ler, mesmo se você for novo em uma empresa que usa a tecnologia .NET.


"- Tudo bem. Definimos essa interface, mas ela vem com nada. Como ela pode ser útil?"

Se você vem de uma linguagem como Javascript ou outra linguagem não fortemente tipada, esse conceito pode parecer estranho.

As interfaces nos permitem abstrair o comportamento desejado da implementação real. Usando um mecanismo conhecido como injeção de dependência (texto em inglês), podemos implementar essas interfaces e isolá-las de outros componentes.

Basicamente, quando você usa injeção de dependência, você define alguns comportamentos usando uma interface. Em seguida, você cria uma classe que implementa a interface. Por fim, você vincula as referências da interface à classe que criou.

" - Parece realmente confuso. Não podemos simplesmente criar uma classe que faça essas coisas para nós?"

Vamos continuar implementando nossa API e você entenderá por que usar essa abordagem.

Altere o código de CategoriesController da seguinte forma:

Eu defini uma função construtora para nosso controlador (um construtor é chamado quando uma nova instância de uma classe é criada) e ela recebe uma instância de ICategoryService. Isso significa que a instância pode ser qualquer coisa que implemente a interface de serviço. Eu armazeno esta instância em um campo privado e somente leitura _categoryService. Usaremos este campo para acessar os métodos de implementação de nosso serviço de categoria.

A propósito, o prefixo de sublinhado é outra convenção comum para denotar um campo. Esta convenção, em especial, não é recomendada pela diretriz oficial de convenção de nomenclatura do .NET, mas é uma prática muito comum como forma de evitar ter que usar a palavra-chave "this" para distinguir campos de classe de variáveis ​​locais. Eu pessoalmente acho que é muito mais fácil de ler, e muitos frameworks e bibliotecas usam essa convenção.

Abaixo do construtor, defini o método que vai tratar as requisições de /api/categories. O atributo HttpGet informa ao pipeline do ASP.NET Core para usá-lo para lidar com solicitações GET (esse atributo pode ser omitido, mas é melhor escrevê-lo para facilitar a legibilidade).

O método usa nossa instância de serviço de categoria para listar todas as categorias e, em seguida, retorna as categorias para o client. O pipeline de estrutura lida com a serialização de dados para um objeto JSON. O tipo IEnumerable<Category> informa à estrutura que queremos retornar uma enumeração de categorias, e o tipo Task, precedido pela palavra-chave async, informa ao pipeline que esse método deve ser executado de maneira assíncrona. Finalmente, quando definimos um método assíncrono, temos que usar a palavra-chave await para tarefas que podem demorar um pouco.

Ok, definimos a estrutura inicial da nossa API. Agora, é preciso realmente implementar o serviço de categorias.

Etapa 4 — Implementando o serviço das categorias

Na pasta raiz da API (a pasta Supermarket.API), crie uma pasta chamada Services. Aqui colocaremos todas as implementações de serviços. Dentro da nova pasta, adicione uma nova classe chamada CategoryService. Altere o código da seguinte forma:

É simplesmente o código básico para a implementação da interface, mas ainda não lidamos com nenhuma lógica. Vamos pensar em como o método de listagem deve funcionar.

Precisamos acessar o banco de dados e retornar todas as categorias, então precisamos retornar esses dados para o client.

Uma classe de serviço não é uma classe que deve manipular o acesso a dados. Existe um padrão chamado Padrão de Repositório (Repository Pattern), que é usado para gerenciar dados de bancos de dados.

Ao usar o Padrão de Repositório, definimos classes de repositório, que basicamente encapsulam toda a lógica para lidar com o acesso aos dados. Esses repositórios expõem métodos para listar, criar, editar e excluir objetos de um determinado modelo, da mesma forma que você pode manipular coleções. Internamente, esses métodos conversam com o banco de dados para realizar operações CRUD, isolando o acesso ao banco de dados do restante da aplicação.

Nosso serviço precisa conversar com um repositório de categorias, para obter a lista de objetos.

Conceitualmente, um serviço pode "conversar" com um ou mais repositórios ou outros serviços para realizar operações.

Pode parecer redundante criar uma outra definição para lidar com a lógica de acesso aos dados, mas você verá daqui a pouco que isolar essa lógica da classe de serviço é realmente vantajoso.

Vamos criar um repositório que será responsável por intermediar a comunicação do banco de dados como forma de persistir as categorias.

Etapa 5 — O repositório de categorias e a camada de persistência

Dentro da pasta Domain, crie um novo diretório chamado Repositories. Em seguida, adicione uma nova interface chamada ICategoryRespository. Defina a interface da seguinte forma:

O código inicial é basicamente idêntico ao código da interface de serviço.

Tendo definida a interface, podemos voltar para a classe de serviço e finalizar a implementação do método de listagem, usando uma instância de ICategoryRepository para retornar os dados.

Agora temos que implementar a lógica real do repositório de categorias. Antes de fazer isso, temos que pensar em como vamos acessar o banco de dados.

A propósito, ainda não temos um banco de dados!

Usaremos o Entity Framework Core (vou chamá-lo de EF Core para simplificar) como nosso ORM de banco de dados. Essa estrutura vem com o ASP.NET Core como seu ORM padrão e expõe uma API amigável que nos permite mapear classes de nossas aplicações para tabelas de banco de dados.

O EF Core também nos permite projetar nossa aplicação primeiro e depois gerar um banco de dados de acordo com o que definimos em nosso código. Essa técnica é chamada de código primeiro (code first). Usaremos a abordagem de código primeiro para gerar um banco de dados (neste exemplo, na verdade, usarei um banco de dados na memória, mas você poderá alterá-lo facilmente para uma instância de servidor SQL Server ou MySQL, por exemplo).

Na pasta raiz da API, crie outro diretório chamado Persistence. Este diretório terá tudo o que precisamos para acessar o banco de dados, como implementações de repositórios.

Dentro da nova pasta, crie outro diretório chamado Contexts e adicione uma nova classe chamada AppDbContext. Essa classe deve herdar DbContext, uma classe que o EF Core usa para mapear seus modelos para tabelas de banco de dados. Altere o código da seguinte maneira:

O construtor que adicionamos a essa classe é responsável por passar a configuração do banco de dados para a classe base por meio de injeção de dependência. Você verá em um momento como isso funciona.

Agora, temos que criar duas propriedades DbSet. Essas propriedades são conjuntos (coleções de objetos exclusivos) que mapeiam modelos para tabelas de banco de dados.

Além disso, temos que mapear as propriedades dos modelos para as respectivas colunas da tabela, especificando quais propriedades são chaves primárias, quais são chaves estrangeiras, os tipos de coluna etc. Podemos fazer isso substituindo o método OnModelCreating, usando um recurso chamado Fluent API (texto em inglês) para especificar o mapeamento do banco de dados. Altere a classe AppDbContext da seguinte maneira:

O código é intuitivo.

Especificamos para quais tabelas nossos modelos devem ser mapeados. Além disso, definimos as chaves primárias, usando o método HasKey, as colunas da tabela, usando o método Property, e algumas restrições como IsRequired, HasMaxLength e ValueGeneratedOnAdd, tudo com expressões lambda de um "modo fluido" (métodos de encadeamento).

Dê uma olhada no seguinte trecho de código:

builder.Entity<Category>()
       .HasMany(p => p.Products)
       .WithOne(p => p.Category)
       .HasForeignKey(p => p.CategoryId);

Aqui estamos especificando um relacionamento entre tabelas. Dizemos que uma categoria tem muitos produtos e definimos as propriedades que vão mapear esse relacionamento (Products, da classe Category, e Category, da classe Product). Também definimos a chave estrangeira (CategoryId).

Dê uma olhada neste tutorial (texto em inglês) se quiser saber como configurar relacionamentos um para um e muitos para muitos usando o EF Core, bem como usá-lo como um todo.

Há também uma configuração para propagação de dados, através do método HasData:

builder.Entity<Category>().HasData

(
  new Category { Id = 100, Name = "Fruits and Vegetables" },
  new Category { Id = 101, Name = "Dairy" }
);

Aqui, nós simplesmente adicionamos duas categorias exemplo por padrão. Isso é necessário para testar nosso endpoint API após finalizá-lo.

Aviso: estamos definindo manualmente as propriedades de Id aqui porque o provedor na memória o exige para que funcione. Estou configurando os identificadores como números grandes para evitar a colisão entre os identificadores gerados automaticamente e os dados iniciais.

Essa limitação não existe em provedores de banco de dados relacionais verdadeiros. Portanto, se você quiser usar um banco de dados como o SQL Server, por exemplo, não precisará especificar esses identificadores. Verifique este relatório de problemas no GitHub se quiser entender esse comportamento.

Tendo implementado a classe de contexto de banco de dados, podemos implementar o repositório de categorias. Adicione uma nova pasta chamada Repositories dentro da pasta Persistence e, então, adicione uma nova classe chamada BaseRepository.

Esta classe é apenas uma classe abstrata que todos os nossos repositórios herdarão. Uma classe abstrata é uma classe que não possui instâncias diretas. Você precisa criar classes diretas para criar as instâncias.

O BaseRepository recebe uma instância do nosso AppDbContext através de injeção de dependência e expõe uma propriedade protegida (uma propriedade que só pode ser acessada pelas classes filhas) chamada _context, que dá acesso a todos os métodos que precisamos para lidar com as operações do banco de dados.

Adicione uma nova classe na mesma pasta chamada CategoryRepository. Agora, vamos realmente implementar a lógica do repositório:

O repositório herda o BaseRepository e implementa o ICategoryRepository.

Observe como é simples implementar o método de listagem. Usamos o conjunto de banco de dados Categories para acessar a tabela de categorias e, então, chamamos o método de extensão ToListAsync, que é responsável por transformar o resultado de uma consulta em uma coleção de categorias.

O EF Core converte nossa chamada de método em uma consulta SQL, da maneira mais eficiente possível. A consulta só é executada quando você chama um método que transformará seus dados em uma coleção, ou quando você usa um método para obter dados específicos.

Agora temos uma implementação limpa do controlador de categorias, do serviço e do repositório.

Separamos os interesses, criando classes que só fazem o que devem fazer.

A última etapa antes de testar a aplicação é vincular nossas interfaces às respectivas classes usando o mecanismo de injeção de dependência do ASP.NET Core.

Etapa 6 — Configurando a injeção de dependência

É hora de você finalmente entender como esse conceito funciona.

Na pasta raiz da aplicação, abra a classe Startup. Esta classe é responsável por configurar todos os tipos de configurações quando a aplicação é iniciada.

Os métodos ConfigureServices e Configure são chamados em tempo de execução pelo pipeline da estrutura para configurar como a aplicação deve funcionar e quais componentes ele deve usar.

Dê uma olhada no método ConfigureServices. Aqui, temos apenas uma linha, que configura a aplicação para que use o pipeline MVC, o que, basicamente, significa que a aplicação vai lidar com requisições e respostas usando classes controller (há mais coisas acontecendo aqui internamente, mas é isso que você precisa saber por enquanto).

Podemos usar o método ConfigureServices, acessando o parâmetro services, para configurar nossas ligações de dependência. Limpe o código da classe removendo todos os comentários e altere o código da seguinte forma:

Veja este trecho de código:

services.AddDbContext<AppDbContext>(options => {

  options.UseInMemoryDatabase("supermarket-api-in-memory");
  
});

Aqui, configuramos o contexto do banco de dados. Dizemos ao ASP.NET Core para usar nosso AppDbContext com uma implementação de banco de dados na memória, que é identificada pela string passada como um argumento para nosso método. Normalmente, o provedor em memória é usado quando escrevemos testes de integração, mas estou usando aqui para simplificar. Dessa forma, não precisamos nos conectar a um banco de dados real para testar a aplicação.

A configuração dessas linhas define internamente nosso contexto de banco de dados para a injeção de dependência usando um tempo de vida com escopo definido.

O tempo de vida com escopo definido informa ao pipeline do ASP.NET Core que sempre que ele precisar resolver uma classe que recebe uma instância de AppDbContext como um argumento de construtor, ele deve usar a mesma instância da classe. Se não houver instância na memória, o pipeline criará uma nova instância e a reutilizará em todas as classes que precisarem dela, durante uma determinada solicitação. Desse modo, você não precisa criar manualmente a instância da classe quando precisar usá-la.

Existem outros escopos vitalícios que você pode conferir lendo a documentação oficial.

A técnica de injeção de dependência nos oferece muitas vantagens, como:

  • Reutilização de código;
  • Melhor produtividade, pois quando temos que mudar a implementação, não precisamos nos preocupar em mudar uma centena de lugares onde você usa esse recurso;
  • Você pode facilmente testar a aplicação, pois podemos isolar o que temos para testar usando mocks (implementação falsa de classes) onde temos que passar interfaces como argumentos do construtor;
  • Quando uma classe precisa receber mais dependências por meio de um construtor, você não precisa alterar manualmente todos os locais onde as instâncias estão sendo criadas (isso é incrível!).

Após configurar o contexto do banco de dados, também vinculamos nosso serviço e repositório às respectivas classes.

services.AddScoped<ICategoryRepository, CategoryRepository>();

services.AddScoped<ICategoryService, CategoryService>();

Aqui, também usamos um tempo de vida com escopo porque essas classes internamente precisam usar a classe de contexto do banco de dados. Faz sentido especificar o mesmo escopo, neste caso.

Agora que configuramos nossas ligações de dependência, temos que fazer uma pequena alteração na classe Program, para que o banco de dados semeie corretamente nossos dados iniciais. Esta etapa só é necessária ao usar o provedor de banco de dados na memória (consulte este relatório de problemas no GitHub para entender o motivo).

Foi necessário alterar o método Main para garantir que nosso banco de dados será "criado" quando a aplicação for iniciada, pois estamos usando um provedor na memória. Sem essa alteração, as categorias que queremos semear não serão criadas.

Com todos os recursos básicos implementados, é hora de testar nosso endpoint de API.

Etapa 7 — Testando a API de categorias

Abra o terminal ou prompt de comando na pasta raiz da API e digite o seguinte comando:

dotnet run

O comando acima inicia a aplicação. O console vai mostrar uma saída similar a esta:

info: Microsoft.EntityFrameworkCore.Infrastructure[10403]

Entity Framework Core 2.2.0-rtm-35687 initialized ‘AppDbContext’ using provider ‘Microsoft.EntityFrameworkCore.InMemory’ with options: StoreName=supermarket-api-in-memory

info: Microsoft.EntityFrameworkCore.Update[30100]

Saved 2 entities to in-memory store.

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]

User profile is available. Using ‘C:\Users\evgomes\AppData\Local\ASP.NET\DataProtection-Keys’ as key repository and Windows DPAPI to encrypt keys at rest.

Hosting environment: Development

Content root path: C:\Users\evgomes\Desktop\Tutorials\src\Supermarket.API

Now listening on: https://localhost:5001

Now listening on: http://localhost:5000

Application started. Press Ctrl+C to shut down.

Você pode ver que o EF Core foi chamado para inicializar o banco de dados. As últimas linhas mostram em quais portas a aplicação está sendo executada.

Abra um navegador e navegue até http://localhost:5000/api/categories (ou para o URL exibido na saída do console). Se você vir um erro de segurança devido ao HTTPS, basta adicionar uma exceção para a aplicação.

O navegador mostrará os seguintes dados JSON como saída:

[
  {
     "id": 100,
     "name": "Fruits and Vegetables",
     "products": []
  },
  {
     "id": 101,
     "name": "Dairy",
     "products": []
  }
]

Aqui vemos os dados que adicionamos ao banco de dados quando configuramos o contexto do banco de dados. Essa saída confirma que nosso código está funcionando.

Você criou um endpoint de GET de API com poucas linhas de código e tem uma estrutura de código que é muito fácil de alterar devido à arquitetura da API.

Agora, é hora de mostrar como é fácil alterar esse código quando você precisa ajustá-lo devido às necessidades do negócio.

Etapa 8 — Criando um recurso de categoria

Se você se lembra da especificação do endpoint da API, notou que nossa resposta JSON real tem uma propriedade extra: um array de produtos. Dê uma olhada no exemplo da resposta desejada:

{
  [
    { "id": 1, "name": "Fruits and Vegetables" },
    { "id": 2, "name": "Breads" },
    … // Outras categorias
  ]
}

O array de produtos está presente em nossa resposta JSON atual porque nosso modelo Category tem uma propriedade Products, necessária ao EF Core para mapear corretamente os produtos de uma determinada categoria.

Não queremos essa propriedade em nossa resposta, mas não podemos alterar nossa classe de modelo para excluir essa propriedade. Isso faria com que o EF Core gerasse erros quando tentamos gerenciar dados de categorias e também interromperia nosso design de modelo de domínio, pois não faz sentido ter uma categoria de produto que não tenha produtos.

Para retornar dados JSON contendo apenas os identificadores e nomes das categorias do supermercado, temos que criar uma classe de recurso.

Uma classe de recurso (texto em inglês) é uma classe que contém apenas informações básicas que serão trocadas entre aplicações de client e terminais de API, geralmente na forma de dados JSON, para representar algumas informações específicas.

Todas as respostas dos endpoints da API devem retornar um recurso.

É uma má prática devolver a representação do modelo real como resposta uma vez que pode conter informações que a aplicação de client não necessita ou que não tem permissão para ter (por exemplo, um modelo de usuário poderia devolver informações da senha do usuário, o que seria um grande problema de segurança).

Precisamos de um recurso para representar apenas nossas categorias, sem os produtos.

Agora que você sabe o que é um recurso, vamos implementá-lo. Antes de tudo, pare a aplicação em execução pressionando Ctrl + C na linha de comando. Na pasta raiz da aplicação, crie uma nova pasta chamada Resources. Lá, adicione uma nova classe chamada CategoryResource.

Temos que mapear nossa coleção de modelos de categoria, que é fornecida pelo nosso serviço de categoria, para uma coleção de recursos de categoria.

Usaremos uma biblioteca chamada AutoMapper para manipular o mapeamento entre objetos. O AutoMapper é uma biblioteca muito popular no mundo .NET e é usada em muitos projetos comerciais e de código aberto.

Digite as seguintes linhas na linha de comando para adicionar o AutoMapper à nossa aplicação:

dotnet add package AutoMapper

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Para usar o AutoMapper, temos que fazer duas coisas:

  • Registrá-lo para injeção de dependência;
  • Criar uma classe que vai dizer ao AutoMapper como manipular o mapeamento de classes.

Primeiramente, abra a classe Startup. No método ConfigureServices, após a última linha, adicione o código a seguir:

services.AddAutoMapper();

Esta linha manipula todas as configurações necessárias do AutoMapper, tal como registrá-la para injeção de dependência e procurar na aplicação durante a inicialização para configurar perfis de mapeamento.

Agora, no diretório raiz, adicione uma nova pasta chamada Mapping e, em seguida, adicione uma classe chamada ModelToResourceProfile. Altere o código desta forma:

A classe herda Profile, um tipo de classe que o AutoMapper usa para verificar como nossos mapeamentos funcionarão. No construtor, criamos um mapa entre a classe de modelo Category e a classe CategoryResource. Como as propriedades das classes têm os mesmos nomes e tipos, não precisamos usar nenhuma configuração especial para elas.

A etapa final consiste em alterar o controlador de categorias para usar AutoMapper para lidar com o mapeamento de nossos objetos.

Alterei o construtor para receber uma instância de implementação de IMapper. Você pode usar esses métodos de interface para usar os métodos de mapeamento do AutoMapper.

Também alterei o método GetAllAsync para mapear nossa enumeração de categorias para uma enumeração de recursos usando o método Map. Este método recebe uma instância da classe ou coleção que queremos mapear e, por meio de definições genéricas de tipos, define para qual tipo de classe ou coleção deve ser mapeada.

Observe que alteramos facilmente a implementação sem precisar adaptar a classe de serviço ou repositório, simplesmente injetando uma nova dependência (IMapper) no construtor.

A injeção de dependência facilita a manutenção e alteração de sua aplicação, pois você não precisa interromper toda a implementação de seu código para adicionar ou remover recursos.

Você provavelmente percebeu que não apenas a classe do controlador, mas todas as classes que recebem dependências (incluindo as próprias dependências) foram resolvidas automaticamente para receber as classes corretas de acordo com as configurações de vinculação.

A injeção de dependência é incrível, não é?


Agora, inicie a API novamente usando o comando dotnet run e acesse http://localhost:5000/api/categories para ver a nova resposta JSON.

0_QhjRK7dOyxX8FXL2
Estes são os dados de resposta que você deveria ver

Nós já temos nosso endpoint GET. Agora, vamos criar outro endpoint para fazer o POST (criação) de categorias.

Etapa 9 — Criando outras categorias

Ao lidar com a criação de recursos, temos que ter cuidado com muitas coisas, tais como:

  • Validação de dados e integração de dados;
  • Autorização para criar recursos;
  • Manipulação de erros;
  • Registro.

Não mostrarei como lidar com autenticação e autorização neste tutorial, mas você pode ver como implementar facilmente esses recursos lendo meu tutorial sobre autenticação de token da Web JSON (texto em inglês).

Além disso, existe um framework muito popular chamada ASP.NET Identity, que fornece soluções internas de segurança e registro de usuários que você pode usar em suas aplicações. Ele inclui provedores para trabalhar com o EF Core, como um IdentityDbContext interno que você pode usar. Você pode aprender mais sobre isso aqui.

Vamos escrever um endpoint HTTP POST que cobrirá os outros cenários (exceto o de registro ou logging, que pode mudar de acordo com diferentes escopos e ferramentas).

Antes de criar outro endpoint, precisamos de um novo recurso. Este recurso vai mapear os dados que as aplicações de client enviam para este endpoint (neste caso, o nome da categoria) para uma classe da nossa aplicação.

Como estamos criando outra categoria, ainda não temos um ID. Isso significa que precisamos de um recurso que represente uma categoria contendo apenas seu nome.

Na pasta Resources, adicione uma nova classe chamada SaveCategoryResource:

Observe os atributos Required e MaxLength aplicados sobre a propriedade Name. Esses atributos são chamados de anotações de dados. O pipeline do ASP.NET Core usa esses metadados para validar solicitações e respostas. Como os nomes sugerem, o nome da categoria é obrigatório e tem um comprimento máximo de 30 caracteres.

Agora vamos definir a forma do novo endpoint de API. Adicione o seguinte código ao controlador de categorias:

Dizemos ao framework que este é um endpoint HTTP POST usando o atributo HttpPost.

Observe o tipo de resposta desse método, Task<IActionResult>. Os métodos presentes nas classes do controlador são chamados de ações e possuem essa assinatura porque podemos retornar mais de um resultado possível após a aplicação executar a ação.

Nesse caso, se o nome da categoria for inválido, ou se algo der errado, teremos que retornar uma resposta de código 400 (solicitação inválida), contendo geralmente uma mensagem de erro que as aplicações de client podem usar para tratar o problema, ou podemos ter um 200 resposta (sucesso) com dados se tudo der certo.

Existem muitos tipos de ação que você pode usar como resposta, mas geralmente podemos usar essa interface, e o ASP.NET Core usará uma classe padrão para isso.

O atributo FromBody informa ao ASP.NET Core para analisar os dados do corpo da solicitação em nossa nova classe de recurso. Isso significa que quando um JSON contendo o nome da categoria é enviado para nossa aplicação, o framework irá analisá-lo automaticamente para nossa nova classe.

Agora, vamos implementar nossa lógica de rota. Temos que seguir alguns passos para criar com sucesso uma nova categoria:

  • Primeiro, temos que validar a solicitação recebida. Se a solicitação for inválida, temos que retornar uma resposta de solicitação inválida contendo as mensagens de erro;
  • Então, se a solicitação for válida, temos que mapear nosso novo recurso para nossa classe de modelo de categoria usando AutoMapper;
  • Agora, precisamos chamar nosso serviço, dizendo para salvar nossa nova categoria. Se a lógica de salvamento for executada sem problemas, ela deverá retornar uma resposta contendo nossos novos dados de categoria. Caso contrário, deve nos dar uma indicação de que o processo falhou e uma possível mensagem de erro;
  • Por fim, se houver um erro, retornamos uma solicitação inválida. Caso contrário, mapeamos nosso novo modelo de categoria para um recurso de categoria e retornamos uma resposta de sucesso ao client, contendo os novos dados de categoria.

Parece ser complicado, mas é realmente fácil implementar essa lógica usando a arquitetura de serviço que estruturamos para nossa API.

Vamos começar validando a solicitação recebida.

Etapa 10 — Validando o corpo da solicitação usando o estado do modelo

Os controladores do ASP.NET Core têm uma propriedade chamada ModelState. Esta propriedade é preenchida durante a execução da requisição antes de chegarmos à execução da nossa ação. É uma instância de ModelStateDictionary, uma classe que contém informações como se a solicitação é válida e possíveis mensagens de erro de validação.

Altere o código do endpoint da seguinte maneira:

O código verifica se o estado do modelo (neste caso, os dados enviados no corpo da solicitação) é válido, verificando nossas anotações de dados. Se não for, a API retornará uma solicitação inválida (com código de status 400) e as mensagens de erro padrão que nossos metadados de anotações forneceram.

O método ModelState.GetErrorMessages() ainda não foi implementado. É um método de extensão (um método que estende a funcionalidade de uma classe ou interface já existente) que vou implementar para converter os erros de validação em strings simples para retornar ao client.

Adicione uma nova pasta Extensions na raiz da nossa API e adicione uma nova classe ModelStateExtensions.

Todos os métodos de extensão devem ser estáticos, assim como as classes onde são declarados. Isso significa que eles não lidam com dados de instância específicos e que são carregados apenas uma vez quando a aplicação é iniciada.

A palavra-chave this na frente da declaração do parâmetro diz ao compilador do C# para tratá-la como um método de extensão. O resultado é que podemos chamá-lo como um método normal desta classe desde que incluamos a respectiva diretiva using onde queremos usar a extensão.

A extensão usa consultas LINQ, um recurso muito útil do .NET que nos permite consultar e transformar dados usando expressões encadeadas. As expressões aqui transformam os métodos de erro de validação em uma lista de strings contendo as mensagens de erro.

Importe o namespace Supermarket.API.Extensions para o controlador de categorias antes de ir para a próxima etapa.

using Supermarket.API.Extensions;

Vamos continuar implementando a lógica do nosso endpoint mapeando nosso recurso para uma classe de modelo de categoria.

Etapa 11 — Mapeando o novo recurso

Nós já definimos um perfil de mapeamento para transformar modelos em recursos. Agora, precisamos de um novo perfil que faz o inverso.

Adicione uma nova classe ResourceToModelProfile na pasta Mapping:

Nada de novo aqui. Graças à mágica da injeção de dependência, o AutoMapper registrará automaticamente esse perfil quando a aplicação for iniciada, e não precisamos alterar nenhum outro local para usá-lo.

Agora podemos mapear nosso novo recurso para a respectiva classe de modelo:

Etapa 12 — Aplicando o padrão solicitação-resposta para manipular a lógica de salvamento

Agora, temos que implementar a lógica mais interessante: salvar uma nova categoria. Esperamos que o nosso serviço faça isso.

A lógica de salvamento pode falhar devido a problemas na conexão com o banco de dados, ou talvez porque alguma regra de negócio interna invalide nossos dados.

Se algo der errado, não podemos simplesmente lançar um erro, porque isso poderia parar a API e a aplicação de client não saberia como lidar com o problema. Além disso, potencialmente teríamos algum mecanismo de log que registraria o erro.

O contrato do método de salvamento, ou seja, a assinatura do método e tipo de resposta, precisa nos indicar se o processo foi executado corretamente. Se o processo der certo, receberemos os dados da categoria. Caso contrário, temos que receber, pelo menos, uma mensagem de erro informando por que o processo falhou.

Podemos implementar esse recurso aplicando o padrão solicitação-resposta. Esse padrão de design corporativo encapsula nossos parâmetros de solicitação e resposta em classes como uma forma de encapsular informações que nossos serviços usarão para processar alguma tarefa e retornar informações para a classe que está usando o serviço.

Esse padrão nos dá algumas vantagens, como:

  • Se precisarmos alterar nosso serviço para receber mais parâmetros, não precisamos quebrar sua assinatura;
  • Podemos definir um contrato padrão para nossa solicitação e/ou respostas;
  • Podemos lidar com a lógica de negócios e possíveis falhas sem interromper o processo da aplicação e não precisaremos usar muitos blocos try-catch.

Vamos criar um tipo de resposta padrão para nossos métodos de serviços que lidam com alterações de dados. Para cada solicitação desse tipo, queremos saber se a solicitação é executada sem problemas. Se falhar, queremos retornar uma mensagem de erro ao client.

Na pasta Domain, dentro de Services, adicione um novo diretório chamado Communication. Adicione uma nova classe chamada BaseResponse.

Essa é uma classe abstrata que nossos tipos de resposta herdarão.

A abstração define uma propriedade Success, que informará se as solicitações foram concluídas com sucesso, e uma propriedade Message, que terá a mensagem de erro se algo falhar.

Observe que essas propriedades são necessárias e somente as classes herdadas podem definir esses dados porque as classes filhas precisam passar essas informações pela função construtora.

Dica: não é uma boa prática definir classes base para tudo, porque as classes base acoplam seu código (texto em inglês) e impedem que você o modifique facilmente. Prefira usar composição sobre herança (texto em inglês).

Para o escopo desta API, não é realmente um problema usar classes base, pois nossos serviços não crescerão muito. Se você perceber que um serviço ou uma aplicação crescerá e mudará com frequência, evite usar uma classe base.

Agora, na mesma pasta, adicione uma nova classe chamada SaveCategoryResponse.

O tipo de respostas também define uma propriedade Category, a qual conterá os dados de nossa categoria se a solicitação for concluída com sucesso.

Note como eu defini três construtores diferentes para esta classe:

  • Um privado, que vai passar os parâmetros de sucesso e mensagem para a classe base, e também define a propriedade Category;
  • Um construtor que recebe apenas a categoria como parâmetro. Este criará uma resposta bem-sucedida, chamando o construtor privado para definir as respectivas propriedades;
  • Um terceiro construtor que especifica apenas a mensagem. Este será usado para criar uma resposta de falha.

Como o C# oferece suporte a vários construtores, simplificamos a criação da resposta sem definir métodos diferentes para lidar com isso, apenas usando construtores diferentes.

Agora podemos alterar nossa interface de serviço para adicionar o novo contrato do método de salvamento.

Altere a interface ICategoryService da seguinte forma:

Simplesmente passaremos uma categoria para este método e ele tratará de toda a lógica necessária para salvar os dados do modelo, orquestrando repositórios e outros serviços necessários para isso.

Observe que não estou criando uma classe de solicitação específica aqui, pois não precisamos de outros parâmetros para realizar essa tarefa. Existe um conceito em programação de computadores chamado KISS (texto em inglês)– abreviação de Keep it Simple, Stupid. Basicamente, ele diz que você deve manter sua aplicação mais simples possível.

Lembre-se disso ao projetar suas aplicações: aplique apenas o que você precisa para resolver um problema. Não exagere na engenharia de sua aplicação.

Agora podemos finalizar a lógica de nosso endpoint:

Após validar os dados da solicitação e mapear o recurso para nosso modelo, passamos para o nosso serviço para persistir os dados.

Se algo falhar, a API retornará uma solicitação incorreta. Caso contrário, a API mapeia a nova categoria (agora incluindo dados como o novo Id) para nosso CategoryResource criado anteriormente e o envia para o client.

Agora, vamos implementar a lógica real para o serviço.

Etapa 13 — A lógica do banco de dados e o padrão unidade de trabalho

Já que faremos a persistência dos dados em um banco de dados, precisamos de um novo método em nosso repositório.

Adicione um novo método AddAsync para a interface ICategoryRepository:

Agora vamos implementar este método em nossa classe de repositório:

Aqui, estamos simplesmente adicionando uma nova categoria ao nosso conjunto.

Quando adicionamos uma classe a um DBSet<>, o EF Core começa a rastrear todas as alterações que acontecem em nosso modelo e usa esses dados no estado atual para gerar consultas que inserirão, atualizarão ou excluirão modelos.

A implementação atual simplesmente adiciona o modelo ao nosso conjunto, mas nossos dados ainda não serão salvos.

Existe um método chamado SaveChanges presente na classe de contexto que temos que chamar para realmente executar as consultas no banco de dados. Eu não chamei aqui porque um repositório não deve fazer a persistência de dados, é apenas uma coleção de objetos na memória (texto em inglês).

Esse assunto é muito controverso até mesmo entre desenvolvedores .NET experientes, mas deixe-me explicar por que você não deve chamar SaveChanges em classes de repositório.

Podemos pensar em um repositório conceitualmente como qualquer outra coleção presente no framework .NET. Ao lidar com uma coleção em .NET (e muitas outras linguagens de programação, como Javascript e Java), você geralmente pode:

  • Adicionar novos itens a ela (como quando você insere dados em listas, vetores e dicionários);
  • Encontrar ou filtrar itens;
  • Remover um item da coleção;
  • Substituir um dado item ou atualizá-lo.

Pense em uma lista do mundo real. Imagine que você está escrevendo uma lista de compras para comprar coisas em um supermercado (que coincidência, não?).

Na lista, você escreve todas as frutas que precisa comprar. Você pode adicionar frutas a esta lista, remover uma fruta se desistir de comprá-la ou substituir o nome de uma fruta. Mas você não pode salvar frutas na lista. Não faz sentido dizer uma coisa dessas naturalmente em português.

Dica: ao projetar classes e interfaces em linguagens de programação orientadas a objetos, tente usar linguagem natural para verificar se o que você está fazendo parece estar correto.

Faz sentido, por exemplo, dizer que um homem implementa uma interface de pessoa, mas não faz sentido dizer que um homem implementa uma conta.

Se você quiser "salvar" as listas de frutas (neste caso, comprar todas as frutas), você paga e o supermercado processa os dados do estoque para verificar se eles têm que comprar mais frutas de um fornecedor ou não.

A mesma lógica pode ser aplicada na programação. Os repositórios não devem salvar, atualizar ou excluir dados. Em vez disso, eles devem delegá-lo a uma classe diferente para lidar com essa lógica.

Há outro problema ao salvar dados diretamente em um repositório: você não pode usar transações.

Imagine que nossa aplicação possui um mecanismo de log que armazena algum nome de usuário e a ação realizada toda vez que é feita uma alteração nos dados da API.

Agora, imagine que, por algum motivo, você tem uma chamada para um serviço que atualiza o nome de usuário (não é um cenário comum, mas vamos considerar).

Você concorda que para alterar o nome de usuário em uma tabela de usuários fictícia, primeiro você precisa atualizar todos os logs para informar corretamente quem executou essa operação, certo?

Imagine, então, que implementamos o método de atualização para usuários e logs em diferentes repositórios, e ambos chamam SaveChanges. O que acontece se um desses métodos falhar no meio do processo de atualização? Você vai acabar com inconsistência de dados.

Devemos salvar nossas alterações no banco de dados somente depois que tudo terminar. Para fazer isso, temos que usar uma transação, que é basicamente um recurso que a maioria dos bancos de dados implementa para salvar dados somente após a conclusão de uma operação complexa.

"- Tudo bem. Então, se não podemos salvar coisas aqui, onde devemos salvá-las?"

Um padrão comum para lidar com esse problema é o Padrão Unidade de Trabalho (Unit of Work Pattern). Esse padrão consiste em uma classe que recebe nossa instância AppDbContext como dependência e expõe métodos para iniciar, concluir ou abortar transações.

Usaremos uma implementação simples de uma unidade de trabalho para abordar nosso problema aqui.

Adicione uma nova interface dentro da pasta Repositories da camada Domain chamada IUnitOfWork:

Como você pode ver, ela só expõe um método que vai concluir de forma assíncrona as operações de gerenciamento de dados.

Agora, vamos adicionar a implementação real.

Adicione uma nova classe chamada UnitOfWork na pasta RepositoriesRepositories da camada Persistence:

Essa é uma implementação simples e limpa que só salvará todas as alterações no banco de dados depois que você terminar de modificá-lo usando seus repositórios.


Se você pesquisar implementações do padrão Unidade de Trabalho, encontrará outras mais complexas implementando operações de reversão (rollback).

Como o EF Core já implementa os padrões repositório e unidade de trabalho nos bastidores, não precisamos nos preocupar com um método de reversão.

"- O que? Então por que nós temos que criar todas essas interfaces e classes?"

Separar a lógica de persistência das regras de negócios oferece muitas vantagens em termos de reutilização e manutenção de código. Se usarmos o EF Core diretamente, acabaremos tendo classes mais complexas que não serão tão fáceis de mudar.

Imagine que no futuro você decida mudar o framework ORM para um diferente, como o Dapper (texto em inglês), por exemplo, ou se tiver que implementar consultas SQL simples por causa do desempenho. Se você acoplar sua lógica de consultas aos seus serviços, será difícil mudar a lógica, pois você terá que fazer isso em muitas classes.

Usando o padrão de repositório, você pode simplesmente implementar uma nova classe de repositório e vinculá-la usando injeção de dependência.

Então, basicamente, se você usar o EF Core diretamente em seus serviços e precisar alterar alguma coisa, é isso que você obterá:

Como eu disse, o EF Core implementa os padrões Repositório e Unidade de Trabalho nos bastidores. Podemos considerar nossas propriedades DbSet<> como repositórios. Além disso, SaveChanges apenas persiste os dados em caso de sucesso para todas as operações do banco de dados.

Agora que você sabe o que é uma unidade de trabalho e por que usá-la com repositórios, vamos implementar a lógica do serviço real.

Graças à nossa arquitetura desacoplada, podemos simplesmente passar uma instância de UnitOfWork como uma dependência para esta classe.

Nossa lógica de negócios é bastante simples.

Primeiro, tentamos adicionar a nova categoria ao banco de dados e depois a API tenta salvá-la, envolvendo tudo dentro de um bloco try-catch.

Se algo falhar, a API chama algum serviço de log fictício e retorna uma resposta indicando falha.

Se o processo terminar sem problemas, a aplicação retornará uma resposta de sucesso, enviando nossos dados de categoria. Simples, certo?

Dica: Em aplicações do mundo real, você não deve envolver tudo dentro de um bloco try-catch genérico, mas sim lidar com todos os erros possíveis separadamente.

Simplesmente adicionar um bloco try-catch não cobrirá a maioria dos possíveis cenários de falha. Certifique-se de corrigir o tratamento de erros do implemento.

A última etapa antes de testar nossa API é vincular a interface de unidade de trabalho à sua respectiva classe.

Adicione esta nova linha ao método ConfigureServices da classe Startup:

services.AddScoped<IUnitOfWork, UnitOfWork>();

Agora vamos testá-la!

Etapa 14 — Testando nosso endpoint de POST usando o Postman

Inicie nossa aplicação novamente usando dotnet run.

Não podemos usar o navegador para testar um endpoint de POST. Vamos usar o Postman para testar nossos endpoints. É uma ferramenta muito útil para testar APIs RESTful.

Abra o Postman e feche as mensagens introdutórias. Você verá uma tela como esta aqui:

0_ZZo6TwEXLK8ngpTV
Tela mostrando as opções para testar endpoints

Altere o GET selecionado por padrão na caixa de seleção por POST.

Digite o endereço da API no campo Enter request URL.

Temos que fornecer os dados do corpo da solicitação a ser enviada à nossa API. Clique no item de menu Body e altere a opção exibida abaixo dela por raw.

O Postman vai mostrar uma opção Text à direita. Altere-a para JSON (application/json) e cole o seguinte dado JSON abaixo:

{
  "name": ""
}
0_8hhlYg26sNJRWmbe
Tela antes de enviar uma solicitação

Como você pode ver, vamos enviar uma string de nome vazio para nosso novo endpoint.

Clique no botão Send. Você vai receber uma saída como esta:

0_eqRGjAtndTIgTyqw
Nossa lógica de validação funciona!

Se lembra da lógica de validação que criamos para o endpoint? Esta saída é a prova de que ela funciona!

Note também o código de status 400 exibido à direita. O resultado BadRequest adiciona automaticamente esse código de status à resposta.

Agora vamos alterar o dado JSON para um válido para ver a nova resposta:

0_z0_hmEcSzvkvbQQX
Finalmente, o resultado que nós esperávamos ter

A API criou corretamente nosso novo recurso.

Até agora, nossa API pode listar e criar categorias. Você aprendeu muitas coisas sobre a linguagem C#, o framework ASP.NET Core e também algumas abordagens comuns de design para estruturar suas APIs.

Vamos continuar nossa API de categorias criando o endpoint para atualizar categorias.

Daqui para frente, já que eu expliquei a maioria dos conceitos, vou acelerar as explicações e me concentrar nos novos assuntos para não gastar seu tempo. Vamos lá!

Etapa 15 — Atualizando categorias

Para atualizar as categorias, precisamos de um endpoint HTTP de PUT.

A lógica que temos que colocar no código é muito similar à do POST:

  • Primeiro, temos que validar a solicitação recebida usando o ModelState;
  • Se a solicitação for válida, a API deve mapear o recurso de entrada para uma classe de modelo usando o AutoMapper;
  • Em seguida, precisamos ligar para o nosso serviço, informando para atualizar a categoria, fornecendo o respectivo Id da categoria e os dados atualizados;
  • Se não houver nenhuma categoria com o Id fornecido no banco de dados, retornaremos uma solicitação inválida. Poderíamos usar um resultado NotFound, mas isso não importa muito para esse escopo, pois fornecemos uma mensagem de erro para as aplicações de client;
  • Se a lógica de salvamento for executada corretamente, o serviço deverá retornar uma resposta contendo os dados da categoria atualizados. Caso contrário, deve nos dar uma indicação de que o processo falhou e uma mensagem indicando o motivo;
  • Por fim, se houver um erro, a API retornará uma solicitação incorreta. Caso contrário, ela mapeia o modelo de categoria atualizado para um recurso de categoria e retorna uma resposta de sucesso à aplicação de client.

Vamos adicionar o novo método PutAsync à classe controladora:

Se você comparar com a lógica do POST, você notará que temos apenas uma diferença aqui: o atributo HttpPut especifica um parâmetro que a rota dada deve receber.

Chamaremos esse endpoint especificando o Id da categoria como o último fragmento de URL, como /api/categories/1. O pipeline do ASP.NET Core analisa esse fragmento para o parâmetro de mesmo nome.

Agora, temos que definir a assinatura do método UpdateAsync na interface ICategoryService:

Passemos para a lógica real.

Etapa 16 — A lógica da atualização

Para atualizar nossa categoria, primeiro precisamos retornar os dados atuais do banco de dados, caso existam. Também precisamos atualizá-los em nosso DBSet<>.

Vamos adicionar dois novos contratos de método à nossa interface ICategoryService:

Definimos o método FindByIdAsync, que retornará de forma assíncrona uma categoria do banco de dados, e o método Update. Preste atenção no fato de que o método Update não é assíncrono, pois a API do EF Core não requer um método assíncrono para atualizar os modelos.

Agora vamos implementar a lógica real na classe CategoryRepository:

Finalmente, podemos adicionar o código para a lógica do serviço:

A API tenta obter a categoria do banco de dados. Se o resultado for null, retornamos uma resposta informando que a categoria não existe. Se a categoria existir, precisamos definir seu novo nome.

A API, então, tenta salvar as alterações, como quando criamos uma nova categoria. Se o processo for concluído, o serviço retornará uma resposta de sucesso. Caso contrário, a lógica de log é executada e o endpoint recebe uma resposta contendo uma mensagem de erro.

Agora vamos ao teste. Primeiro, vamos adicionar uma nova categoria para ter um Id válido para usar. Poderíamos usar os identificadores das categorias que propagamos em nosso banco de dados, mas quero fazer dessa maneira para mostrar que nossa API atualizará o recurso correto.

Execute a aplicação novamente e, usando o Postman, envie um POST de uma nova categoria para o banco de dados:

0_UIjyDcw4lnRqY072
Adicionando uma nova categoria para atualizá-la posteriormente

Com um Id válido em mãos, altere a opção POST para PUT na caixa de seleção e adicione o valor do ID no final da URL. Altere a propriedade name para um nome diferente e envie a solicitação para verificar o resultado:

0_i2cpHjrW-422v7jt
Os dados da categoria foram atualizados com sucesso

Você pode enviar uma solicitação GET para o endpoint da API para garantir que você editor corretamente o nome da categoria:

0_3b8rR_shOGcI6P8y
Este é agora o resultado de uma solicitação GET

A última operação que nós temos que implementar para as categorias é a exclusão de categorias. Vamos fazer isso criando um endpoint de HTTP Delete.

Etapa 17 — Excluindo categorias

A lógica para excluir categorias é realmente fácil de implementar já que a maioria dos métodos que precisamos foi construída anteriormente.

Estas são as etapas necessárias para nossa rota funcionar:

  • A API precisa chamar nosso serviço, informando para excluir nossa categoria, fornecendo o respectivo Id;
  • Se não houver nenhuma categoria com o ID informado no banco de dados, o serviço deverá retornar uma mensagem indicando isso;
  • Se a lógica de exclusão for executada sem problemas, o serviço deve retornar uma resposta contendo nossos dados de categoria excluídos. Caso contrário, deve nos dar uma indicação de que o processo falhou e uma possível mensagem de erro;
  • Por fim, se houver um erro, a API retornará uma solicitação inválida. Caso contrário, a API mapeia a categoria atualizada para um recurso e retorna uma resposta de sucesso ao client.

Vamos começar adicionando a lógica do novo endpoint:

O atributo HttpDelete também define um modelo de id.

Antes de adicionar a assinatura DeleteAsync à nossa interface ICategoryService, precisamos fazer uma pequena refatoração.

O novo método de serviço deve retornar uma resposta contendo os dados da categoria, da mesma forma que fizemos para os métodos PostAsync e UpdateAsync. Poderíamos reutilizar o SaveCategoryResponse para essa finalidade, mas não estamos salvando dados neste caso.

Para evitar a criação de uma nova classe com a mesma forma para atender a esse requisito, podemos simplesmente renomear nosso SaveCategoryResponse para CategoryResponse.

Se estiver usando o Visual Studio Code, você pode abrir a classe SaveCategoryResponse, colocar o cursor do mouse acima do nome da classe e usar a opção Change All Occurrences para renomear a classe:

0*0vbRNdSBgQqsf-TO
Forma fácil de alterar o nome em todos os arquivos

Certifique-se de renomear o nome de arquivo também.

Vamos adicionar a assinatura do método DeleteAsync à interface ICategoryService:

Antes de implementar a lógica de exclusão, precisamos de um novo método em nosso repositório.

Adicione a assinatura do método Remove à interface ICategoryRepository:

void Remove(Category category);

E agora adicione a implementação real na classe do repositório:

O EF Core requer que a instância do nosso modelo seja passada para o método Remove para entender corretamente qual modelo estamos excluindo, em vez de simplesmente passar um Id.

Finalmente, vamos implementar a lógica na classe CategoryService:

Não há nada de novo aqui. O serviço tenta encontrar a categoria por ID e então chama nosso repositório para excluir a categoria. Finalmente, a unidade de trabalho completa a transação executando a operação real no banco de dados.

"- Ei, mas e os produtos de cada categoria? Você não precisa criar um repositório e excluir os produtos primeiro, para evitar erros?"

A resposta é não. Graças ao mecanismo de acompanhamento do EF Core, quando carregamos um modelo do banco de dados, a estrutura sabe quais relacionamentos o modelo possui. Se o excluirmos, o EF Core saberá que deve excluir todos os modelos relacionados primeiro, recursivamente.

Podemos desabilitar esse recurso ao mapear nossas classes para tabelas de banco de dados, mas está fora do escopo deste tutorial. Dê uma olhada aqui (texto em inglês) se você quiser aprender sobre esse recurso.

Agora é hora de testar nosso novo endpoint. Execute a aplicação novamente e envie uma solicitação DELETE usando o Postman da seguinte maneira:

0_Ju431skSI6l5IOvV
Como você pode ver, a API excluiu a categoria existente sem problemas

Podemos conferir se nossa API está funcionando corretamente enviando uma solicitação GET:

0_v8PzsTC57h3uIlN1
Agora, recebemos apenas uma categoria como resultado

Finalizamos a API de categorias. Agora é hora de passar para a API de produtos.

Etapa 18 — A API de produtos

Até agora, você aprendeu como implementar todos os verbos HTTP básicos para lidar com operações CRUD com ASP.NET Core. Vamos para o próximo nível implementando nossa API de produtos.

Não detalharei todos os verbos HTTP novamente porque seria exaustivo. Para a parte final deste tutorial, abordarei apenas a solicitação GET, para mostrar como incluir entidades relacionadas ao consultar dados do banco de dados e como usar os atributos Description que definimos para os valores de enumeração EUnitOfMeasurement.

Adicione um novo controlador na pasta Controllers chamada ProductsController.

Antes de codificar qualquer coisa aqui, temos que criar o recurso do produto.

Deixe-me refrescar sua memória mostrando novamente como nosso recurso deve ficar:

{
 [
  {
   "id": 1,
   "name": "Sugar",
   "quantityInPackage": 1,
   "unitOfMeasurement": "KG"
   "category": {
   "id": 3,
   "name": "Sugar"
   }
  },
  … // Outros produtos
 ]
}

Queremos um array em JSON contendo todos os produtos do banco de dados.

Os dados em JSON diferem do modelo do produto por duas coisas:

  • A unidade de medida é exibida em uma forma mais curta, mostrando apenas sua abreviação;
  • Emitimos os dados da categoria sem incluir a propriedade CategoryId.

Para representar a unidade de medida, podemos usar uma propriedade de string simples em vez de um tipo enum (a propósito, não temos um tipo enum padrão para dados JSON. Então, temos que transformá-lo em um tipo diferente).

Agora que sabemos como moldar o novo recurso, vamos criá-lo. Adicione uma nova classe ProductResource na pasta Resources:

Agora temos que configurar o mapeamento entre a classe de modelo e nossa nova classe de recurso.

A configuração de mapeamento será quase a mesma usada para outros mapeamentos, mas aqui temos que lidar com a transformação de nosso enum EUnitOfMeasurement em uma string.

Você se lembra do atributo StringValue aplicado sobre os tipos de enumeração? Agora, vou mostrar como extrair essas informações usando um recurso poderoso do framework .NET: a API Reflection (texto em inglês).

A API Reflection é um poderoso conjunto de recursos que nos permite extrair e manipular metadados. Muitas estruturas e bibliotecas (incluindo o próprio ASP.NET Core) usam esses recursos para lidar com muitas coisas nos bastidores.

Agora, vamos ver como isso funciona na prática. Adicione uma nova classe na pasta Extensions chamada EnumExtensions.

Pode parecer assustador na primeira vez que você olhar para o código, mas não é tão complexo. Vamos detalhar a definição de código para entender como ele funciona.

Primeiramente, definimos um método genérico (um método que pode receber mais de um tipo de argumento, neste caso, representado pela declaração TEnum) que recebe um determinado enum como argumento.

Como enum é uma palavra-chave reservada em C#, adicionamos um @ na frente do nome do parâmetro para torná-lo um nome válido.

A primeira etapa de execução desse método é obter as informações de tipo (classe, interface, enum ou definição de estrutura) do parâmetro usando o método GetType.

Em seguida, o método obtém o valor de enumeração específico (por exemplo, Kilogram) usando GetField(@enum.ToString()).

A próxima linha encontra todos os atributos de Description aplicados sobre o valor de enumeração e armazena seus dados em uma matriz (podemos especificar vários atributos para uma mesma propriedade em alguns casos).

A última linha usa uma sintaxe mais curta para verificar se temos pelo menos um atributo de descrição para o tipo de enumeração. Se tivermos, retornaremos o valor Description fornecido por este atributo. Caso contrário, retornamos a enumeração como uma string, usando a conversão padrão.

O operador ?. (um operador condicional nulo) verifica se o valor é null antes de acessar sua propriedade.

O operador ?? (um operador de coalescência nula) informa à aplicação para retornar o valor à esquerda se não estiver vazio ou o valor à direita caso contrário.

Como temos um método de extensão para extrair descrições, vamos configurar nosso mapeamento entre modelo e recurso. Graças ao AutoMapper, podemos fazê-lo com apenas uma linha extra.

Abra a classe ModelToResourceProfile e altere o código desta forma:

Essa sintaxe diz ao AutoMapper para usar o novo método de extensão para converter nosso valor EUnitOfMeasurement em uma string contendo sua descrição. Simples, certo? Você pode ler a documentação oficial (em inglês) para entender a sintaxe completa.

Observe que não definimos nenhuma configuração de mapeamento para a propriedade da categoria. Como configuramos anteriormente o mapeamento para categorias e porque o modelo de produto possui uma propriedade de categoria do mesmo tipo e nome, o AutoMapper sabe implicitamente que deve mapeá-lo usando a respectiva configuração.

Agora, vamos adicionar o código do endpoint. Altere o código ProductsController:

Basicamente, a mesma estrutura definida para o controlador de categorias.

Vamos para a parte do serviço. Adicione uma nova interface IProductService à pasta Services presente na camada Domain:

Você deve ter percebido que precisamos de um repositório antes de realmente implementar o novo serviço.

Adicione uma nova interface chamada IProductRepository à respectiva pasta:

Agora vamos implementar o repositório. Temos que implementá-lo quase da mesma forma que fizemos para o repositório de categorias, exceto que precisamos retornar os respectivos dados de categoria de cada produto ao consultar os dados.

O EF Core, por padrão, não inclui entidades relacionadas aos seus modelos ao consultar dados porque pode ser muito lento (imagine um modelo com dez entidades relacionadas, todas as entidades relacionadas com seus próprios relacionamentos).

Para incluir os dados das categorias, precisamos apenas de uma linha extra:

Observe a chamada para Include(p => p.Category). Podemos encadear essa sintaxe para incluir quantas entidades forem necessárias ao consultar dados. O EF Core vai convertê-lo em uma junção ao realizar a seleção.

Agora podemos implementar a classe ProductService da mesma forma que fizemos para as categorias:

Vamos vincular as dependências alterando a classe Startup:

Finalmente, antes de testar a API, vamos alterar a classe AppDbContext para incluir alguns produtos quando inicializar a aplicação de modo que podemos ver os resultados:

Adicionei dois produtos fictícios associando-os às categorias que criamos quando da inicialização da aplicação.

É hora de testar! Execute a API novamente e envie uma solicitação GET para /api/products usando Postman:

1_ztoCnav4f95YwGJnQkiuxQ
Conseguimos! Aqui estão nossos produtos

É isso! Parabéns!

Agora, você tem uma base sobre como construir uma API RESTful usando ASP.NET Core usando uma arquitetura desacoplada. Você aprendeu muitas coisas sobre a estrutura do .NET Core, como trabalhar com C#, os conceitos básicos do EF Core e AutoMapper e muitos padrões úteis para usar ao projetar suas aplicações.

Você pode verificar a implementação completa da API, contendo os demais verbos HTTP para produtos, verificando o repositório do GitHub (em inglês):

evgomes/supermarket-api
API RESTful simples criada com ASP.NET Core 2.2 para mostrar como criar serviços RESTful usando uma arquitetura desacoplada e de fácil manutenção.

Conclusão

O ASP.NET Core é um ótimo framework para usar ao criar aplicações da web. Ele vem com muitas APIs úteis que você pode usar para criar aplicações limpas e de fácil manutenção. Considere-o como uma opção ao criar aplicações profissionais.

Este artigo não abordou todos os aspectos de uma API profissional, mas você aprendeu todos os conceitos básicos. Você também aprendeu muitos padrões úteis para resolver padrões que enfrentamos diariamente.

Espero que tenha gostado deste artigo e espero que tenha sido útil para você. Agradeço seu feedback para entender como posso melhorar isso.

Referências para continuar aprendendo

Tutoriais de .NET — Microsoft Docs

Documentação do ASP.NET Core — Microsoft Docs