Backend com Minimal APIs
📽 Veja esta vídeo-aula no Youtube
Este material uma o método *Minimal API* e só funciona em .NET 6 ou superior. Caso use uma versão anterior, ou queira conhecer o método *MVC*, veja este material.
Criando o projeto usando template web
Vamos usar o template web
para iniciar nosso projeto, e depois personalizá-lo.
dotnet new web
Serão criados diversos arquivos, entre eles:
nome-do-projeto.csproj
: Arquivo base do projetoappsettings.json
eappsettings.Development.json
: Configurações da aplicaçãoProperties/launchSettings.json
: Configurações do servidor HTTP embutidoProgram.cs
: Arquivo inicial da aplicação
Arquivo .csproj
Conteúdos:
<Project Sdk="Microsoft.NET.Sdk.Web">
indica que é um projeto web. Em aplicações console o SDK usado é diferente (Microsoft.NET.Sdk
). Isso permitirá que sejam usadas funcionalidades HTTP.<TargetFramework>net6.0</TargetFramework>
indica que a aplicação necessita do runtime do .NET 6 para executar. Veja esta aula para mais detalhes.<Nullable>enable</Nullable>
e<ImplicitUsings>enable</ImplicitUsings>
ativam duas funcionalidade opcionais do C# em suas versões mais recentes.
Program.cs
Arquivo base da aplicação, no formato do .NET 6. Veja esta aula para mais detalhes.
Código gerado:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Executando a aplicação
Faça exatamente como em uma aplicação console:
dotnet run
Você verá algo do tipo:
C:\etec\MinApiHelloWorld>dotnet run
Compilando...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7262
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5281
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\ermogenes\code\etec\web\2022\MinApiHelloWorld\
Now listening on
indica os URLs pelos quais a aplicação responderá.Application started. Press Ctrl+C to shut down.
indica que a aplicação está em execução e pode ser finalizada usandoCtrl+C
.
Acessando um dos URLs indicados você será redirecionado ao seu navegador padrão.
Configurando HTTPS
Caso encontre o erro abaixo, você não tem certificado de desenvolvimento:
Compilando...
Unhandled exception. System.InvalidOperationException: Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found or is out of date.
Corrija usando dotnet dev-certs https
.
Mais opções:
dotnet dev-certs https --check
verifica seus certificados de desenvolvimentodotnet dev-certs https
cria um certificados de desenvolvimento auto-assinadodotnet dev-certs https --check --trust
verifica seus certificados de desenvolvimento confiáveisdotnet dev-certs https --clean
exclui os certificados de desenvolvimentodotnet dev-certs https --trust
cria um certificados de desenvolvimento auto-assinado e adiciona como confiável
Para forçar o redirecionamento para https
caso o usuário envie http
, use:
app.UseHttpsRedirection();
WebApplication
e middlewares
A variável app
, do tipo WebApplication
, representa nossa aplicação. Ela é ativada pelo método Run()
, quando começa a esperar por requisições HTTP. É criada pelo método Build()
de um objeto construtor, que recebe a maioria das configurações. Muitas configurações são efetuadas usando o mecanismo de serviços do ASP.NET.
Antes de executar, podemos adicionar funcionalidades ao servidor, chamadas de middlewares. Cada requisição é submetida aos middlewares pertinentes para realizar as ações desejadas. Adicionamos novos middlewares para ensinar nossa aplicação a tratar requisições de formas diferentes.
No template padrão é criado um único middleware:
app.MapGet("/", () => "Hello World!");
Ela cria uma rota (endpoint) que atende à requisições HTTP de verbo GET na URL “/”, executando uma função que retorna a string "Hello World"
.
HTTP
O HTTP (Hypertext Transfer Protocol) é o protocolo de comunicação sobre o qual a web funciona. Com ele navegadores, servidores, aplicativos mobile e qualquer outro tipo de aplicação podem trocar informações de maneira simples e direta.
Por exemplo, quando você quer acessar um site, você digita seu endereço (ou URL) em um navegador (cliente HTTP) e ele envia seu pedido (GET) para o servidor indicado na URL, que responde e esse resultado é exibido pelo navegador. Porém, HTTP é muito mais que isso, suportando muitos tipos de tráfego de informação.
Uma requisição (request) usa um método (method) ou verbo que indica a ação desejada e aponta para um URL (com um caminho de recurso, em um servidor). Pode conter um conjunto de cabeçalhos (headers) com a configuração da comunicação, e um corpo com informações adicionais. A resposta (response) possui um código de status indicando o sucesso/fracasso da comunicação, cabeçalhos opcionais, e o corpo da mensagem contendo o conteúdo requisitado.
Clientes HTTP
Usamos clientes HTTP toda vez que fazemos uma requisição a um servidor usando esse protocolo. O tipo mais conhecido é o navegador (browser), mas ele tem um comportamento com finalidade específica, e não serve para tudo que precisamos como desenvolvedor. Podemos fazer nossas chamadas manualmente com JavaScript usando Fetch, mas isso não é nada prático para testar as nossas comunicações com os backends.
Também é possível realizar chamadas Fetch usando um gerador de documentação conhecido como Swagger. Ele usa os padrões OpenAPI.
Caso seja necessário baixe um cliente HTTP dedicado para desenvolvedores chamado Insomnia. Com ele podemos entender em detalhes o que acontece na comunicação. Baixe-o e instale-o acessando https://insomnia.rest/download/, opção Insomnia Core. Outra opção bastante utilizada é o Postman.
Métodos HTTP
Quando fazemos uma requisição a um URL, devemos indicar o método desejado. Cada método é indicado por um verbo, e possui uma semântica própria.
no recurso indicado
Alguns verbos importantes:
Verbo | Semântica
— | —
GET
| Solicita os dados do recurso indicado
POST
| Envia dados novos
PUT
| Substitui os dados existentes no recurso indicado pelos dados novos enviados
PATCH
| Aplica modificações parciais ao dados existentes no recurso indicado
DELETE
| Remove o recurso indicado
Veja uma tabela completa aqui.
HTTP Status Codes
Os códigos de status seguem uma tabela numérica, com o seguinte agrupamento:
- Respostas de informação (100-199)
- Respostas de sucesso (200-299)
- Redirecionamentos (300-399)
- Erros do cliente (400-499)
- Erros do servidor (500-599)
Por exemplo:
200 OK
caso a solicitação seja válida e o resultado seja enviado com sucesso404 NOT FOUND
caso o recurso não exista400 BAD REQUEST
caso a solicitação seja inválida (por erro do cliente)500 INTERNAL SERVER ERROR
caso ocorra um problema (por erro do servidor)
Veja uma tabela completa aqui. Veja também 🐱 aqui e 🐶 aqui.
Logging
Podemos usar Console.WriteLine
em nossa aplicação web, mas há um mecanismo mais interessante.
Muitas vezes pode ser necessário gerar logs na aplicação de outras maneiras que não no console. Usando o Logger, podemos decidir posteriormente a melhor maneira de tratar isso.
Há vários níveis de log que podem ser criados:
app.Logger.LogInformation
- Informaçãoapp.Logger.LogWarning
- Avisoapp.Logger.LogError
- Erroapp.Logger.LogCritical
- Erro crítico
Exemplos:
app.Logger.LogInformation("Aplicação iniciada");
app.Logger.LogWarning("Algo estranho aconteceu. Verifique.");
app.Logger.LogError("Erro na aplicação. Corrija.");
app.Logger.LogCritical("Erro crítico. Corrija agora.");
OpenAPI (Swagger)
Para podermos rapidamente testar nossos endpoints, bem como para mantermos uma documentação padronizada para nossa API podemos utilizar o padrão OpenAPI, conhecido como Swagger.
Adicione o pacote:
dotnet add package Swashbuckle.AspNetCore
Adicione os seguintes serviços na configuração da aplicação:
AddSwaggerGen
- gera o arquivo.jspn
com a documentação da APIAddEndpointsApiExplorer
- gera uma página para navegação
Adicione também os middlewares:
Exemplo:
using Swashbuckle.AspNetCore;
...
builder.Services.AddSwaggerGen();
builder.Services.AddEndpointsApiExplorer();
...
app.UseSwagger();
app.UseSwaggerUI();
...
Isso adiciona o atendimento à URL /swagger
.
Roteamento
Podemos criar endpoints usando quaisquer métodos HTTP. Os mais comuns:
app.MapGet("/api", () => "Resposta ao método GET");
app.MapPost("/api", () => "Resposta ao método POST");
app.MapPut("/api", () => "Resposta ao método PUT");
app.MapDelete("/api", () => "Resposta ao método DELETE");
app.MapMethods("/api", new[] { "PATCH" }, () => "Resposta ao método PATCH");
Nos exemplos acima, as chamadas atendem a verbos diferentes na mesma URL (/api
).
Parâmetros
Podemos receber informações juntamente com a requisição através do uso de parâmetros.
Vamos usar neste curso anotações para indicar a origem dos parâmetros. Para isso, adicione esta referência:
using Microsoft.AspNetCore.Mvc;
Também podemos usar nomes diferentes no parâmetro e na variavel.
Ex.:
... [From_____(Name = "max-registos")] int maxRegistros ...
FromQuery
Dados contidos como variáveis após o endpoint base.
Por exemplo, fazendo:
app.MapGet("/api", ([FromQuery] int x, [FromQuery] string y) => {
return $"Recebidos x={x} e y={y}.";
});
Teremos acesso às variáveis x
e y
em uma chamada GET a /api?x=valorX&y=valorY
.
FromRoute
Dados contidos como variáveis dentro do endpoint base.
Por exemplo, fazendo:
app.MapGet("/api/{x}/abc/{y}", ([FromRoute] int x, [FromRoute] string y) => {
return $"Recebidos x={x} e y={y}.";
});
Teremos acesso às variáveis x
e y
em uma chamada GET a /api/valorX/abc/valorY
.
FromBody
Dados contidos no corpo da mensagem, em formato JSON.
Como só pode haver um FromBody
por endpoint, precisamos criar uma classe para receber mais de um.
Por exemplo, fazendo:
app.MapPost("/api", ([FromBody] DadosEntrada dadosEntrada) => {
return $"Recebidos x={dadosEntrada.x} e y={dadosEntrada.y}.";
});
...
public class DadosEntrada
{
public int x { get; set; }
public string? y { get; set; }
}
Teremos acesso às variáveis x
e y
em uma chamada GET a /api
com o corpo:
{
"x": 42,
"y": "olá universo"
}
FromServices
Dados injetados no servidor.
Uma aplicação bastante comum é injetar o contexto de banco de dados em todas as rotas, para não ter que criá-las várias vezes.
Por exemplo, fazendo:
...
builder.Services.AddDbContext<clientesContext>(opt =>
{
string connectionString = builder.Configuration.GetConnectionString("clientesConnection");
var serverVersion = ServerVersion.AutoDetect(connectionString);
opt.UseMySql(connectionString, serverVersion);
});
...
app.MapGet("/api", ([FromServices] clientesContext db) => {
return db.Cliente.ToList<Cliente>();
});
O endpoint terá acesso ao contexto de banco de dados através de um serviço injetado a cada requisição.
Resultados
Tipos possíveis de retornos:
Results.Redirect(url)
redireciona para outra URL.Results.StatusCode(n)
retorna uma resposta vazia, com códigon
.Results.Text(string)
retorna uma string, com código 200.Results.Ok(objeto)
retorna um objeto serializado em JSON, com código 200.Results.NotFound()
retorna uma resposta vazia, código 404.Results.BadRequest(objeto)
retorna um objeto serializado em JSON, com código 400.Results.Created(...)
usado para retornar o resultado de uma inclusão REST.
Exemplo:
app.MapGet("/api/hello-world", () =>
{
return Results.Ok(new {
mensagem = "Hello, World!",
dataHora = DateTime.Now
});
});
Exceções
Para ter visibilidade dos erros ocorridos na aplicação, podemos usar o middleware UseDeveloperExceptionPage()
para retornar uma tela com os detalhes da exceção. O ideal é fazer isso somente em ambiente de desenvolvimento, e não em produção.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
Para testar, gere uma exceção manualmente:
app.MapGet("/vai-dar-m", () => { throw new Exception("Oops... 💩"); });
Servindo arquivos estáticos
Podemos adicionar middlewares que entregam arquivos diretamente. Usamos esta funcionalidade para criar um servidor web, onde podemos hospedar nosso frontend.
Exemplo:
...
app.UseDefaultFiles();
app.UseStaticFiles();
...
UseDefaultFiles()
indica que a aplicação deve entregar arquivos estáticos. Eles deverão estar em/wwwroot
.UseStaticFiles()
indica que a aplicação deve entregar arquivos com nomes padrão (comoindex.html
) quando o nome de arquivo não for indicado.
Por exemplo, caso tenhamos a seguinte estrutura de arquivos:
Serão atendidos os os URLs:
/
, comindex.html
./index.html
, comindex.html
./style.css
, comstyle.css
./index.js
, comindex.js
./imagens/logo.png
, comimagens/logo.png
.
HelloWorld com frontend e backend
Backend: Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSwaggerGen();
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapGet("/hello-world", () =>
{
return Results.Ok(new {
mensagem = "Hello, World!"
});
});
app.Run();
Marcação: wwwroot/index.html
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello Dev Web</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<img src="imagens/logo-dev-web.png" alt="Dev Web" />
<h1>Temos uma mensagem para você:</h1>
<div id="mensagem"></div>
<script src="index.js"></script>
</body>
</html>
Estilização: wwwroot/style.css
body {
display: flex;
flex-flow: column;
align-items: center;
}
body > * {
flex: 1;
}
#mensagem {
color: #004545;
font-size: 2em;
}
Script: wwwroot/index.js
document.addEventListener("DOMContentLoaded", async () => {
const mensagem = document.getElementById("mensagem");
const response = await fetch("/hello-world");
const result = await response.json();
mensagem.innerHTML = result.mensagem;
});
Imagem: wwwroot/imagens/logo.png
Resultado: