View on GitHub

aulas-programacao-web

Materiais de Aula - Programação de Computadores com tecnologias Web

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:

Arquivo .csproj

Conteúdos:

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\

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:

Para forçar o redirecionamento para httpscaso 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:

Por exemplo:

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:

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:

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:

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();
...

Por exemplo, caso tenhamos a seguinte estrutura de arquivos:

Serão atendidos os os URLs:

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: