← voltar ao índice

· 34 min de leitura · por Tiago

ASP.NET Core 10: pipeline HTTP por dentro

ASP.NET Core 10: pipeline HTTP por dentro

Na aula anterior, abrimos a caixa-preta do .NET e acompanhamos o caminho entre o código C#, o compilador, IL, assemblies, runtime, JIT, Garbage Collector e código nativo.

Agora vamos subir uma camada.

Em vez de perguntar apenas como o processo .NET executa nosso código, vamos perguntar:

O que realmente acontece quando uma requisição HTTP chega a uma aplicação ASP.NET Core?

É muito fácil criar uma API moderna com poucas linhas:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.Run();

Esse código funciona, mas esconde muita coisa.

Quando alguém acessa a aplicação, existe um fluxo semelhante a este:

Cliente
  |
  | HTTP
  v
Sistema operacional
  |
  v
Kestrel
  |
  v
ASP.NET Core request pipeline
  |
  +--> Middleware 1
  |
  +--> Middleware 2
  |
  +--> Routing
  |
  +--> Authentication
  |
  +--> Authorization
  |
  v
Endpoint
  |
  v
Application code
  |
  v
HTTP Response

A ordem desses componentes muda completamente o comportamento da aplicação.

Um middleware colocado no lugar errado pode impedir autenticação, quebrar CORS, esconder exceções, gerar logs incompletos ou executar trabalho desnecessário.

Uma configuração lida diretamente de appsettings.json pode funcionar localmente e falhar em containers.

Um endpoint pode parecer simples, mas estar dependendo de model binding, injeção de dependência, serialização, routing, logging e tratamento global de erros.

Por isso, antes de avançarmos para Web APIs, Minimal APIs, Dependency Injection e programação assíncrona como temas próprios, precisamos compreender a infraestrutura que conecta tudo isso.

ASP.NET Core é o framework web moderno do ecossistema .NET. Ele fornece os componentes fundamentais para construir APIs, aplicações web e serviços, incluindo hosting, servidores HTTP, middleware, routing, configuração, logging e integração com o container de Dependency Injection. A documentação atual do ASP.NET Core 10 organiza esses elementos justamente como fundamentos da plataforma.

Nesta aula vamos construir uma pequena API chamada Logby.Store.Api e usá-la como laboratório para enxergar o pipeline acontecendo.

O modelo mental mais importante desta aula

Antes de escrever código, guarde esta imagem:

A requisição não pula diretamente para o endpoint.

Request
  |
  v
Server
  |
  v
Middleware pipeline
  |
  v
Endpoint selecionado pelo routing
  |
  v
Seu código
  |
  v
Middleware pipeline no caminho de volta
  |
  v
Response

A parte que muita gente esquece é esta:

no caminho de volta

Um middleware pode executar código antes e depois do próximo componente.

Conceitualmente:

app.Use(async (context, next) =>
{
    Console.WriteLine("Antes");

    await next();

    Console.WriteLine("Depois");
});

Essa estrutura parece pequena, mas é a base de recursos como logging, tratamento de exceções, autenticação, autorização, compressão, headers de segurança e várias outras preocupações transversais.

A documentação define o pipeline do ASP.NET Core como uma sequência de request delegates executados em ordem, na qual cada componente pode executar lógica antes e depois do próximo, ou encerrar o pipeline antecipadamente.

Vamos ver isso funcionando.

Passo 1: criando a aplicação ASP.NET Core

Crie uma aplicação vazia:

dotnet new web \n    -n Logby.Store.Api \n    --framework net10.0

cd Logby.Store.Api

O template web é interessante para esta aula porque começa com pouca coisa.

Abra o Program.cs.

Você encontrará algo semelhante a:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

São poucas linhas.

Mas cada uma possui uma responsabilidade importante.

Vamos separar mentalmente o arquivo em três fases:

1. Construção do host e configuração de serviços

var builder = WebApplication.CreateBuilder(args);

2. Construção da aplicação

var app = builder.Build();

3. Configuração do pipeline e endpoints

app.MapGet(...);

4. Inicialização do servidor

app.Run();

Essa divisão será útil durante toda a série.

WebApplication.CreateBuilder faz muito mais do que criar um objeto

Esta linha:

var builder = WebApplication.CreateBuilder(args);

prepara a infraestrutura necessária para construir a aplicação.

O modelo moderno de hosting do ASP.NET Core utiliza WebApplicationBuilder e WebApplication como uma forma simplificada de configurar e executar aplicações sobre o Generic Host. O host é responsável por aspectos como inicialização, ciclo de vida da aplicação, configuração, logging e integração de serviços.

O builder nos dá acesso, entre outras coisas, a:

builder.Services;
builder.Configuration;
builder.Environment;
builder.Logging;

Cada propriedade representa uma parte importante da infraestrutura.

builder.Services
registro de dependências

builder.Configuration
configuração da aplicação

builder.Environment
ambiente atual

builder.Logging
configuração de logging

Mais tarde teremos uma aula inteira sobre Dependency Injection. Por enquanto, basta entender que a fase do builder é onde normalmente configuramos o que a aplicação precisa para existir.

Por exemplo:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails();

var app = builder.Build();

Antes de Build(), estamos configurando serviços.

Depois de Build(), estamos configurando principalmente o comportamento da aplicação em execução.

builder.Build() cria a aplicação executável

Quando fazemos:

var app = builder.Build();

obtemos uma instância de WebApplication.

A partir daqui começamos a montar o pipeline HTTP e mapear endpoints.

Pense no processo desta forma:

WebApplicationBuilder
        |
        | configura
        v
services + configuration + logging + host
        |
        v
Build()
        |
        v
WebApplication
        |
        +--> middleware pipeline
        +--> endpoints
        +--> lifetime
        +--> server integration

É uma simplificação, mas ajuda a separar configuração de construção.

app.Run() mantém a aplicação viva

No final encontramos:

app.Run();

Aqui a aplicação inicia sua execução como aplicação web e permanece aguardando trabalho até que seja encerrada.

Se removêssemos essa etapa, não teríamos o servidor executando normalmente para atender requisições.

Agora precisamos entender quem recebe essas requisições.

Passo 2: Kestrel, a porta de entrada HTTP

Uma aplicação ASP.NET Core precisa de uma implementação de servidor HTTP.

No cenário padrão moderno, esse papel é exercido pelo Kestrel.

Kestrel é o servidor web multiplataforma recomendado para ASP.NET Core e é configurado por padrão nos templates do framework. Ele recebe conexões HTTP e disponibiliza as requisições para a aplicação ASP.NET Core processá-las.

Podemos visualizar:

Browser / Mobile / Another API
              |
              | HTTP
              v
           Kestrel
              |
              v
         ASP.NET Core
              |
              v
           Endpoint

Quando executamos:

dotnet run

veremos logs semelhantes a:

Now listening on: http://localhost:xxxx
Application started.
Hosting environment: Development

O endereço exato pode variar conforme configuração local e launchSettings.json.

Kestrel pode ter seus endpoints de escuta configurados por código, configuração, argumentos de linha de comando e outras fontes. A documentação também destaca que launchSettings.json é voltado ao desenvolvimento local, não deve ser tratado como configuração de produção.

Essa diferença é importante.

Muitos problemas de deployment começam porque alguém pensa:

"Funcionou na porta configurada no launchSettings.json,
logo funcionará igual no container."

Não necessariamente.

O ambiente de produção pode definir a porta por variável de ambiente, configuração do host, container ou plataforma cloud.

Kestrel precisa sempre de Nginx ou IIS na frente?

Não.

Essa é uma ideia herdada de arquiteturas e versões mais antigas.

Kestrel pode ser exposto diretamente como servidor de borda em cenários suportados, mas também é muito comum colocá-lo atrás de um reverse proxy ou serviço gerenciado.

Por exemplo:

Internet
   |
   v
Azure Front Door
   |
   v
Load Balancer / Reverse Proxy
   |
   v
Kestrel
   |
   v
ASP.NET Core

Ou:

Internet
   |
   v
Nginx
   |
   v
Kestrel

Ou ainda:

Internet
   |
   v
Cloud platform ingress
   |
   v
Container
   |
   v
Kestrel

A escolha depende da infraestrutura, TLS, balanceamento, headers encaminhados, proteção de borda e outros requisitos operacionais.

O ponto desta aula é simples:

Kestrel é o servidor HTTP que conecta a rede ao pipeline da nossa aplicação.

Passo 3: cada requisição ganha um HttpContext

Quando uma requisição entra no pipeline, ASP.NET Core trabalha com um objeto central:

HttpContext

Ele representa o contexto daquela requisição HTTP.

Dentro dele encontramos conceitos como:

HttpContext
   |
   +--> Request
   +--> Response
   +--> User
   +--> Items
   +--> RequestServices
   +--> TraceIdentifier
   +--> Connection

Vamos criar um endpoint para observar algumas informações:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/debug/request", (HttpContext context) =>
{
    return Results.Ok(new
    {
        method = context.Request.Method,
        path = context.Request.Path.Value,
        scheme = context.Request.Scheme,
        host = context.Request.Host.Value,
        traceId = context.TraceIdentifier
    });
});

app.Run();

Execute:

dotnet run

E faça uma requisição:

GET /debug/request HTTP/1.1
Host: localhost

A resposta será semelhante a:

{
  "method": "GET",
  "path": "/debug/request",
  "scheme": "http",
  "host": "localhost:5000",
  "traceId": "0H..."
}

O valor de traceId, porta e outros campos variam a cada ambiente e requisição.

Por que HttpContext importa?

Porque praticamente tudo que acontece durante uma requisição está conectado a ele direta ou indiretamente.

Um middleware pode ler:

context.Request.Headers;

Uma autenticação pode definir:

context.User;

Um endpoint pode escrever:

context.Response.StatusCode;

Um componente pode acessar serviços daquela requisição por:

context.RequestServices;

Mas existe uma armadilha importante.

HttpContext representa estado de uma requisição específica. Não devemos capturá-lo e armazená-lo para uso arbitrário depois que a requisição terminou. Também não devemos presumir que seja seguro acessá-lo concorrentemente de qualquer forma.

Uma boa regra é:

use HttpContext como contexto da requisição,
não como banco de dados global da aplicação.

Passo 4: entendendo middleware de verdade

Agora chegamos ao coração do ASP.NET Core.

Um middleware é um componente do pipeline HTTP.

A requisição atravessa os componentes na ordem em que foram registrados.

Imagine:

app.Use(MiddlewareA);
app.Use(MiddlewareB);
app.Use(MiddlewareC);

A execução conceitual pode ser:

Request
  |
  v
A antes
  |
  v
B antes
  |
  v
C antes
  |
  v
Endpoint
  |
  v
C depois
  |
  v
B depois
  |
  v
A depois
  |
  v
Response

Esse comportamento acontece porque cada middleware pode chamar o próximo componente e continuar sua execução depois que ele retorna.

Vamos provar isso.

Substitua o Program.cs:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    Console.WriteLine("Middleware A: antes");

    await next();

    Console.WriteLine("Middleware A: depois");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("Middleware B: antes");

    await next();

    Console.WriteLine("Middleware B: depois");
});

app.MapGet("/", () =>
{
    Console.WriteLine("Endpoint executado");

    return Results.Ok(new
    {
        message = "Hello ASP.NET Core"
    });
});

app.Run();

Ao chamar /, veremos algo semelhante:

Middleware A: antes
Middleware B: antes
Endpoint executado
Middleware B: depois
Middleware A: depois

A documentação oficial descreve exatamente esse comportamento: os componentes formam uma cadeia, podem executar trabalho antes e depois do próximo componente e a ordem de registro define a ordem de execução.

Por que isso importa?

Porque a ordem não é estética.

Compare:

app.UseAuthentication();
app.UseAuthorization();

com:

app.UseAuthorization();
app.UseAuthentication();

São pipelines diferentes.

Autorização normalmente precisa que a identidade já tenha sido estabelecida pela autenticação.

Se invertermos componentes que dependem semanticamente uns dos outros, podemos criar bugs difíceis de perceber.

Use, Run e Map: três comportamentos que você precisa distinguir

ASP.NET Core possui diferentes formas de compor o pipeline.

Use

Use normalmente participa da cadeia e pode chamar o próximo componente.

app.Use(async (context, next) =>
{
    await next();
});

Run

Run é terminal.

Ele não recebe next.

app.Run(async context =>
{
    await context.Response.WriteAsync(
        "Pipeline encerrado aqui");
});

Depois disso, não existe próximo componente naquela cadeia.

Map

Map pode ramificar o pipeline com base em caminho.

app.Map("/health", healthApp =>
{
    healthApp.Run(async context =>
    {
        await context.Response.WriteAsync("OK");
    });
});

Uma forma de pensar:

Use
participa da cadeia

Run
encerra a cadeia

Map
cria uma ramificação

Não confunda app.Map(...) usado para ramificar pipeline com os métodos de mapeamento de endpoints como MapGet, MapPost e MapGroup. Eles possuem propósitos relacionados ao processamento de requisições, mas atuam em abstrações diferentes.

Passo 5: short-circuit, quando a requisição não chega ao endpoint

Um middleware não é obrigado a chamar next().

Isso permite encerrar a requisição antecipadamente.

Vamos criar uma proteção extremamente simples apenas para aprender o conceito:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    if (!context.Request.Headers.TryGetValue(
        "X-Demo-Key",
        out var apiKey) || apiKey != "logby-secret")
    {
        context.Response.StatusCode =
            StatusCodes.Status401Unauthorized;

        await context.Response.WriteAsJsonAsync(new
        {
            error = "Missing or invalid demo key"
        });

        return;
    }

    await next();
});

app.MapGet("/orders", () =>
{
    return Results.Ok(new[]
    {
        new
        {
            id = Guid.NewGuid(),
            total = 125.50m
        }
    });
});

app.Run();

Sem o header:

GET /orders

receberemos 401.

Com:

GET /orders
X-Demo-Key: logby-secret

a requisição continua.

O ponto importante é este trecho:

return;

O middleware não chama:

await next();

Portanto, o restante do pipeline não é executado.

Isso é chamado de short-circuit.

Não use esse exemplo como autenticação real

Esse middleware existe apenas para demonstrar o fluxo.

Autenticação real envolve esquemas, handlers, validação de credenciais, identidade e políticas. Teremos uma aula específica sobre autenticação e JWT.

A lição aqui é outra:

qualquer middleware pode impedir que o endpoint seja alcançado.

Quando um endpoint "misteriosamente não executa", investigue o pipeline antes de culpar o routing.

Passo 6: routing, como uma URL encontra código executável

Considere:

GET /orders/7e28c57f-61d2-4a61-b9e6-1c1c730ca216

Como ASP.NET Core decide qual código deve processar essa requisição?

Essa é a responsabilidade do routing.

Routing combina informações da requisição com endpoints registrados e seleciona o endpoint executável apropriado. A documentação do ASP.NET Core descreve o routing como o mecanismo responsável por corresponder requisições recebidas e encaminhá-las para endpoints executáveis.

Exemplo:

app.MapGet(
    "/orders/{id:guid}",
    (Guid id) =>
    {
        return Results.Ok(new
        {
            id,
            status = "Pending"
        });
    });

Temos vários elementos aqui:

GET
método HTTP

/orders/{id:guid}
padrão da rota

id
route value

:guid
route constraint

handler
código executado quando ocorre o match

Se chamarmos:

GET /orders/abc

abc não atende a constraint guid.

A rota não será considerada um match válido da mesma forma que um GUID.

Routing não deveria validar regra de negócio

Route constraints ajudam a distinguir formatos de rota.

Elas não deveriam ser usadas como substituto para validação de domínio.

Por exemplo:

quantity precisa ser maior que zero

é uma regra diferente de:

este segmento da URL precisa ter formato de inteiro

Não tente colocar todas as regras no template da rota.

Endpoint não é sinônimo de controller

Em ASP.NET Core, endpoint é uma abstração mais ampla.

Pode existir endpoint implementado por:

Minimal API
MVC Controller
Razor Pages
Health Checks
outros componentes integrados ao routing

Por isso, quando falamos:

routing seleciona um endpoint

não estamos dizendo necessariamente:

routing seleciona um controller

Essa distinção será útil quando compararmos Controllers e Minimal APIs nas próximas aulas.

Passo 7: construindo uma API de laboratório mais realista

Vamos criar um pequeno serviço em memória.

Crie Order.cs:

public sealed record Order(
    Guid Id,
    string CustomerName,
    decimal Total,
    string Status);

Crie OrderStore.cs:

public sealed class OrderStore
{
    private readonly Dictionary<Guid, Order> _orders = [];

    public OrderStore()
    {
        var firstOrder = new Order(
            Guid.Parse("8ff54ce9-807c-41a5-8ee6-f796972e0fb6"),
            "Maria",
            125.50m,
            "Pending");

        var secondOrder = new Order(
            Guid.Parse("fb032a42-1863-41a5-aa5d-b1124eff585f"),
            "João",
            899.90m,
            "Paid");

        _orders[firstOrder.Id] = firstOrder;
        _orders[secondOrder.Id] = secondOrder;
    }

    public IReadOnlyCollection<Order> GetAll()
    {
        return _orders.Values.ToArray();
    }

    public Order? GetById(Guid id)
    {
        return _orders.GetValueOrDefault(id);
    }
}

Agora configure o Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<OrderStore>();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

app.Use(async (context, next) =>
{
    var startedAt = DateTimeOffset.UtcNow;

    await next();

    var elapsed =
        DateTimeOffset.UtcNow - startedAt;

    app.Logger.LogInformation(
        "HTTP {Method} {Path} returned {StatusCode} in {ElapsedMs} ms",
        context.Request.Method,
        context.Request.Path,
        context.Response.StatusCode,
        elapsed.TotalMilliseconds);
});

app.MapGet("/orders", (OrderStore store) =>
{
    return Results.Ok(store.GetAll());
});

app.MapGet(
    "/orders/{id:guid}",
    (Guid id, OrderStore store) =>
    {
        var order = store.GetById(id);

        return order is null
            ? Results.NotFound()
            : Results.Ok(order);
    });

app.Run();

Agora temos várias peças trabalhando juntas.

AddSingleton<OrderStore>()

Registramos um serviço no container de Dependency Injection.

Nesta aula não vamos aprofundar lifetime, porque teremos um capítulo dedicado a isso.

O importante agora é observar que o endpoint recebe:

OrderStore store

sem fazer:

new OrderStore()

ASP.NET Core integra o container de Dependency Injection ao fluxo da aplicação e resolve dependências registradas para os componentes que participam desse modelo.

AddProblemDetails()

Registra serviços relacionados à geração padronizada de respostas de erro no formato Problem Details.

UseExceptionHandler()

Adiciona tratamento global de exceções ao pipeline.

UseStatusCodePages()

Permite produzir conteúdo para determinados status codes que ainda não possuem corpo de resposta.

A documentação de tratamento de erros de APIs mostra a combinação de AddProblemDetails, UseExceptionHandler e UseStatusCodePages como infraestrutura para respostas de erro consistentes em aplicações ASP.NET Core.

Middleware de tempo

Nosso middleware mede o tempo total do restante do pipeline:

var startedAt = DateTimeOffset.UtcNow;

await next();

var elapsed =
    DateTimeOffset.UtcNow - startedAt;

O ponto crucial é que medimos antes de next() e calculamos depois.

Assim capturamos o tempo do que aconteceu adiante no pipeline.

Passo 8: por que a ordem do middleware pode mudar tudo

Considere um pipeline futuro:

app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

Não existe uma regra universal dizendo que toda aplicação precisa exatamente dessa lista.

Mas existe uma regra universal mais importante:

middleware deve ser ordenado de acordo com suas dependências e com o comportamento que desejamos observar.

Imagine um middleware de logging colocado depois de um componente que encerra a requisição.

Request
  |
  v
Middleware que retorna 401
  |
  X
Logging middleware nunca executa

Talvez isso seja exatamente o que você queria.

Talvez seja um bug.

Agora imagine tratamento de exceções colocado muito tarde:

Request
  |
  v
Middleware A lança exceção
  |
  X
Exception handler estava depois

Um middleware só consegue tratar adequadamente exceções de componentes que estão adiante na cadeia e cuja execução ocorre dentro de seu fluxo de chamada.

Por isso, tratamento global de exceções normalmente precisa estar cedo o suficiente para envolver o restante relevante do pipeline.

Pense em middleware como composição de funções

Uma forma útil de visualizar:

ExceptionHandler(
    Logging(
        Authentication(
            Authorization(
                Endpoint
            )
        )
    )
)

A ordem do código constrói essa composição.

Isso torna muito mais fácil raciocinar sobre:

quem vê a requisição primeiro?

quem vê a resposta por último?

quem consegue capturar a exceção de quem?

quem pode interromper o fluxo antes de quem?

Passo 9: escrevendo um middleware como classe

Lambdas são ótimas para experimentos pequenos.

Quando a lógica cresce, podemos criar uma classe dedicada.

Crie CorrelationIdMiddleware.cs:

public sealed class CorrelationIdMiddleware
{
    private const string HeaderName =
        "X-Correlation-Id";

    private readonly RequestDelegate _next;
    private readonly ILogger<CorrelationIdMiddleware> _logger;

    public CorrelationIdMiddleware(
        RequestDelegate next,
        ILogger<CorrelationIdMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId =
            context.Request.Headers.TryGetValue(
                HeaderName,
                out var value)
                && !string.IsNullOrWhiteSpace(value)
                    ? value.ToString()
                    : Guid.NewGuid().ToString("N");

        context.Response.Headers[HeaderName] =
            correlationId;

        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["CorrelationId"] = correlationId
        }))
        {
            await _next(context);
        }
    }
}

Registre:

app.UseMiddleware<CorrelationIdMiddleware>();

Agora toda requisição possui um correlation ID recebido do cliente ou gerado pela aplicação.

O middleware também devolve esse identificador na resposta.

O que está acontecendo no construtor?

RequestDelegate next

representa o próximo componente da cadeia.

ILogger<CorrelationIdMiddleware> logger

é uma dependência fornecida pela infraestrutura de DI.

Middlewares convencionais ativados dessa forma são construídos com dependências explícitas e participam do pipeline por meio do RequestDelegate. A documentação de middleware também destaca a integração com Dependency Injection e a importância de considerar o ciclo de vida das dependências utilizadas pelo componente.

Por que correlation ID importa?

Imagine uma requisição que passa por:

API Gateway
   |
   v
Orders API
   |
   v
Payment API
   |
   v
Message Broker

Sem algum mecanismo de correlação, investigar um problema distribuído pode virar uma caça ao tesouro.

Mais adiante, quando estudarmos observabilidade, veremos tracing distribuído e OpenTelemetry, que oferecem mecanismos muito mais completos.

Neste momento, o correlation ID é um exercício excelente para entender middleware e escopo de logging.

Passo 10: logging estruturado desde o início

Um erro comum é tratar logging como:

Console.WriteLine(
    "Pedido encontrado: " + order.Id);

ASP.NET Core integra o sistema de logging do .NET e fornece ILogger, com suporte a categorias, níveis e logging estruturado por templates de mensagem. A documentação atual recomenda o uso da API ILogger para monitorar comportamento e diagnosticar problemas.

Exemplo:

app.MapGet(
    "/orders/{id:guid}",
    (
        Guid id,
        OrderStore store,
        ILogger<Program> logger
    ) =>
    {
        var order = store.GetById(id);

        if (order is null)
        {
            logger.LogWarning(
                "Order {OrderId} was not found",
                id);

            return Results.NotFound();
        }

        logger.LogInformation(
            "Order {OrderId} found with status {Status}",
            order.Id,
            order.Status);

        return Results.Ok(order);
    });

Observe:

"Order {OrderId} found with status {Status}"

Não estamos apenas montando uma string.

Estamos declarando propriedades estruturadas:

OrderId
Status

Dependendo do provider de logging, essas propriedades podem ser indexadas e pesquisadas como campos.

Isso será essencial em produção.

Nunca registre tudo sem pensar

Logging é observabilidade, mas também pode virar vazamento de dados.

Evite registrar indiscriminadamente:

passwords
access tokens
API keys
cookies de autenticação
dados pessoais desnecessários
documentos confidenciais
prompts sensíveis
respostas completas de modelos

Quando chegarmos a IA, esse cuidado será ainda maior.

Um prompt pode conter dados internos da empresa.

Não transforme logging em exfiltração acidental.

Passo 11: configuração não é apenas appsettings.json

Vamos criar uma configuração para a aplicação.

appsettings.json:

{
  "Store": {
    "Currency": "USD",
    "MaxOrdersPerRequest": 100
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Poderíamos ler diretamente:

var currency =
    builder.Configuration["Store:Currency"];

Isso funciona.

Mas para configurações relacionadas, o Options Pattern costuma oferecer um contrato mais forte.

Crie StoreOptions.cs:

public sealed class StoreOptions
{
    public const string SectionName = "Store";

    public string Currency { get; set; } =
        "USD";

    public int MaxOrdersPerRequest { get; set; }
}

Configure:

builder.Services
    .AddOptions<StoreOptions>()
    .BindConfiguration(
        StoreOptions.SectionName)
    .Validate(
        options =>
            !string.IsNullOrWhiteSpace(
                options.Currency),
        "Store currency is required.")
    .Validate(
        options =>
            options.MaxOrdersPerRequest > 0,
        "MaxOrdersPerRequest must be greater than zero.")
    .ValidateOnStart();

Agora podemos consumir:

using Microsoft.Extensions.Options;

app.MapGet(
    "/settings",
    (IOptions<StoreOptions> options) =>
    {
        return Results.Ok(options.Value);
    });

A configuração padrão do ASP.NET Core combina múltiplos providers, incluindo arquivos JSON, variáveis de ambiente e argumentos de linha de comando. A precedência permite que fontes posteriores sobrescrevam valores anteriores. Para grupos relacionados de configuração, a documentação recomenda o Options Pattern, que também permite encapsular e validar settings.

Por que isso importa em containers?

Localmente podemos ter:

{
  "Store": {
    "Currency": "USD"
  }
}

Em produção, podemos sobrescrever com variável de ambiente:

Store__Currency=USD

O duplo underscore é importante para representar hierarquia de configuração de forma compatível entre plataformas.

Assim, não precisamos gerar um appsettings.json diferente para cada pod ou container.

O mesmo binário pode receber configurações diferentes por ambiente.

Nunca coloque segredo real no repositório

Isto é configuração:

{
  "Store": {
    "MaxOrdersPerRequest": 100
  }
}

Isto é segredo:

{
  "OpenAI": {
    "ApiKey": "sk-secret-value"
  }
}

Não trate os dois da mesma forma.

A documentação do ASP.NET Core diferencia configuração comum de informações confidenciais e recomenda mecanismos específicos, como Secret Manager no desenvolvimento e soluções de cofre de segredos em produção.

Teremos uma aula específica sobre Azure Key Vault.

Por enquanto, guarde:

appsettings.json não é um cofre de segredos.

Passo 12: ambientes de execução

Uma aplicação pode precisar de comportamentos diferentes em:

Development
Staging
Production

ASP.NET Core trata ambiente como um conceito de primeira classe e disponibiliza essa informação através do host. Quando nenhuma configuração de ambiente é fornecida, Production é o padrão.

Podemos verificar:

if (app.Environment.IsDevelopment())
{
    app.Logger.LogInformation(
        "Application running in Development");
}

Ou:

app.MapGet("/environment", (
    IWebHostEnvironment environment) =>
{
    return Results.Ok(new
    {
        environment.EnvironmentName
    });
});

Um uso comum é habilitar diagnóstico detalhado apenas em desenvolvimento.

Por quê?

Porque páginas ou respostas detalhadas de exceção podem expor:

stack traces
paths internos
detalhes de implementação
nomes de classes
informações de infraestrutura

Em produção, queremos registrar detalhes internamente, mas retornar respostas controladas ao cliente.

Ambiente não deve substituir feature flag

Um erro comum é fazer:

if (environment.IsProduction())
{
    EnableNewCheckout();
}

O ambiente responde:

onde estou executando?

Feature flag responde:

esta funcionalidade está habilitada?

São conceitos diferentes.

Misturar os dois torna deployments e rollouts mais difíceis.

Passo 13: tratamento global de erros

Considere este endpoint:

app.MapGet("/explode", () =>
{
    throw new InvalidOperationException(
        "Something went wrong");
});

Sem estratégia adequada, erros podem gerar respostas inconsistentes e detalhes sensíveis.

Em uma API, queremos normalmente algo mais previsível.

Configure:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

app.MapGet("/orders/{id:guid}", (Guid id) =>
{
    return Results.NotFound();
});

app.MapGet("/explode", () =>
{
    throw new InvalidOperationException(
        "Unexpected failure");
});

app.Run();

Problem Details fornece uma estrutura padronizada para representar erros HTTP em APIs.

Uma resposta pode assumir forma semelhante a:

{
  "type": "about:blank",
  "title": "An error occurred while processing your request.",
  "status": 500
}

O formato exato depende da configuração e do cenário.

No ASP.NET Core 10, a infraestrutura de error handling continua integrando middleware de exceção e Problem Details para produzir respostas consistentes quando configurado.

Não faça try/catch em cada endpoint

Isto tende a produzir repetição:

app.MapGet("/orders/{id}", async (Guid id) =>
{
    try
    {
        // lógica
    }
    catch (Exception exception)
    {
        // log
        // resposta 500
    }
});

Depois repetimos em 50 endpoints.

Preocupações globais devem ser tratadas em fronteiras adequadas.

Isso não significa que todo try/catch é errado.

Capture exceções localmente quando você consegue tomar uma decisão útil:

retry específico
fallback
compensação
tradução de erro
tratamento esperado de integração

Mas erros inesperados devem chegar a um tratamento global consistente.

Passo 14: request pipeline e resposta, o ponto de não retorno

HTTP possui uma característica importante para o pipeline.

Depois que os headers da resposta começaram a ser enviados, nossa capacidade de modificar status code e headers fica limitada.

Imagine:

app.Use(async (context, next) =>
{
    await context.Response.WriteAsync(
        "Beginning response
");

    await next();

    context.Response.StatusCode = 500;
});

Essa abordagem pode falhar conceitualmente porque a resposta pode já ter começado antes da tentativa de alterar seu status.

Uma boa regra:

não trate a resposta HTTP como um objeto que pode ser reescrito livremente até o último instante.

Streaming torna isso ainda mais importante.

Quando chegarmos a aplicações de chat com IA, poderemos começar a enviar tokens ao cliente antes que a geração termine.

Nesse momento, se uma falha ocorrer no meio do streaming, já não existe a mesma liberdade de transformar tudo em uma resposta JSON 500 tradicional.

A arquitetura de erros muda quando a resposta é progressiva.

Esse é um exemplo de como fundamentos de ASP.NET Core se conectam diretamente com aplicações de IA.

Passo 15: um pipeline mais próximo de produção

Vamos montar uma versão final do laboratório.

Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<OrderStore>();

builder.Services
    .AddOptions<StoreOptions>()
    .BindConfiguration(
        StoreOptions.SectionName)
    .Validate(
        options =>
            !string.IsNullOrWhiteSpace(
                options.Currency),
        "Store currency is required.")
    .Validate(
        options =>
            options.MaxOrdersPerRequest > 0,
        "MaxOrdersPerRequest must be greater than zero.")
    .ValidateOnStart();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

app.UseMiddleware<CorrelationIdMiddleware>();

app.Use(async (context, next) =>
{
    var stopwatch =
        System.Diagnostics.Stopwatch.StartNew();

    await next();

    stopwatch.Stop();

    app.Logger.LogInformation(
        "HTTP {Method} {Path} returned {StatusCode} in {ElapsedMs} ms",
        context.Request.Method,
        context.Request.Path,
        context.Response.StatusCode,
        stopwatch.Elapsed.TotalMilliseconds);
});

app.MapGet("/health", () =>
{
    return Results.Ok(new
    {
        status = "Healthy"
    });
});

app.MapGet("/orders", (OrderStore store) =>
{
    return Results.Ok(store.GetAll());
});

app.MapGet(
    "/orders/{id:guid}",
    (
        Guid id,
        OrderStore store,
        ILogger<Program> logger
    ) =>
    {
        var order = store.GetById(id);

        if (order is null)
        {
            logger.LogWarning(
                "Order {OrderId} was not found",
                id);

            return Results.NotFound();
        }

        return Results.Ok(order);
    });

app.Run();

Agora visualize a execução de:

GET /orders/8ff54ce9-807c-41a5-8ee6-f796972e0fb6
X-Correlation-Id: client-request-42

Conceitualmente:

1. Cliente abre conexão HTTP

2. Kestrel recebe a requisição

3. ASP.NET Core cria o contexto da requisição

4. Exception handler envolve o pipeline seguinte

5. CorrelationIdMiddleware lê ou cria o identificador

6. Middleware de timing inicia medição

7. Routing encontra o endpoint compatível

8. Parâmetro id é obtido da rota

9. OrderStore é resolvido pelo DI

10. ILogger é resolvido pelo DI

11. Handler executa

12. Resultado é convertido em resposta HTTP

13. Middleware de timing registra duração

14. Correlation ID é devolvido no header

15. Resposta volta pelo Kestrel ao cliente

Esse é o tipo de modelo mental que queremos construir.

Não precisamos decorar classes internas do framework.

Precisamos entender as fronteiras.

O que acontece quando duas requisições chegam ao mesmo tempo?

Essa pergunta é importante.

Uma aplicação web não deve ser imaginada como:

requisição 1
termina
requisição 2
começa
termina
requisição 3

Na prática, várias requisições podem estar em andamento simultaneamente.

Isso muda completamente a forma como tratamos estado compartilhado.

Nosso exemplo usa:

builder.Services.AddSingleton<OrderStore>();

Isso significa que a mesma instância pode ser utilizada por várias requisições.

O Dictionary<Guid, Order> do nosso laboratório não foi escolhido para representar um armazenamento concorrente de produção.

Se começarmos a permitir escrita simultânea, precisamos analisar thread safety.

Por exemplo, isto pode ser perigoso:

public void Add(Order order)
{
    _orders.Add(order.Id, order);
}

quando várias requisições alteram a coleção concorrentemente sem uma estratégia apropriada.

Esta é uma lição essencial:

lifetime de dependência e concorrência estão conectados.

Quando estudarmos Dependency Injection, vamos entender profundamente Transient, Scoped e Singleton.

Por enquanto, nunca trate singleton como "uma forma de economizar new".

Singleton significa estado compartilhado durante a vida da aplicação.

Isso tem consequências.

HttpContext.Items: útil, mas não transforme em gaveta universal

Podemos armazenar dados válidos apenas durante uma requisição:

context.Items["CorrelationId"] =
    correlationId;

Depois outro componente pode ler:

var correlationId =
    context.Items["CorrelationId"];

Isso pode ser útil para comunicação simples entre componentes do pipeline.

Mas existe um trade-off.

Strings mágicas e object enfraquecem type safety.

context.Items["user"]
context.Items["User"]
context.Items["currentUser"]

Agora temos três convenções diferentes.

Para dados importantes do domínio ou serviços complexos, prefira abstrações explícitas.

Use Items quando a semântica realmente for contexto efêmero da requisição.

Middleware ou endpoint filter?

Mais adiante veremos filtros em detalhes, mas vale construir uma primeira distinção.

Use middleware quando a preocupação é naturalmente HTTP e transversal ao pipeline.

Exemplos:

correlation
exception handling
request logging
headers
compression
autenticação no pipeline

Considere filtros ou comportamentos mais próximos do endpoint quando a preocupação depende da execução do endpoint ou de seus argumentos.

Exemplos possíveis:

validação específica de handlers
política aplicada a grupo de endpoints
lógica que precisa observar parâmetros do endpoint

Não existe uma tabela perfeita para todos os frameworks de endpoint.

A pergunta é:

em qual fronteira esta regra pertence?

Colocar tudo em middleware pode criar um pipeline gigante.

Colocar tudo dentro de endpoints duplica preocupações transversais.

Arquitetura é encontrar a fronteira adequada.

Middleware não deveria conhecer todo o domínio

Imagine:

app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments(
        "/orders"))
    {
        // consulta banco
        // valida estoque
        // calcula desconto
        // valida cliente
        // decide regra de pagamento
    }

    await next();
});

Isso é um sinal de alerta.

Middleware é excelente para preocupações transversais de infraestrutura HTTP.

Regras de negócio pertencem a componentes de aplicação e domínio.

Caso contrário, acabamos com uma arquitetura assim:

Middleware
   |
   +--> HTTP
   +--> negócio
   +--> banco
   +--> cache
   +--> pagamento
   +--> logging

Tudo misturado.

Uma das metas desta série será separar responsabilidades sem criar abstrações desnecessárias.

launchSettings.json não vai para produção para controlar sua aplicação

Ao criar o projeto, você verá:

Properties/
  launchSettings.json

Esse arquivo é usado por tooling e desenvolvimento local para configurar perfis de execução, URLs e variáveis usadas durante o desenvolvimento.

A própria documentação de endpoints do Kestrel destaca que essas configurações de launch são voltadas ao ambiente local.

Em produção, configuração costuma vir de:

environment variables
container settings
cloud platform configuration
command line
appsettings apropriado
secret stores
orchestrators

Esse detalhe parece pequeno até o primeiro deployment em Docker.

Então aparece:

"Minha API usa localhost:7182.
Por que o container não responde?"

Porque localhost dentro de uma máquina, container ou pod possui significado de rede específico daquele ambiente.

Teremos uma aula inteira sobre Docker e networking.

Reverse proxy e headers encaminhados

Imagine:

Client
  |
  | HTTPS
  v
Reverse Proxy
  |
  | HTTP interno
  v
Kestrel

Do ponto de vista do Kestrel, a conexão direta pode parecer HTTP.

Mas o cliente original usou HTTPS.

Também podemos ter:

IP original do cliente
host original
protocolo original

precisando ser encaminhados pelo proxy.

É por isso que arquiteturas com reverse proxy precisam configurar corretamente forwarded headers e confiança nos proxies conhecidos.

Nunca assuma automaticamente que qualquer header X-Forwarded-* recebido da internet é confiável.

Headers podem ser forjados.

A aplicação precisa conhecer sua topologia de infraestrutura.

Essa preocupação será aprofundada quando chegarmos a cloud e API Management.

Armadilhas comuns que você encontrará em projetos reais

1. Colocar middleware na ordem errada

Esse é provavelmente o problema mais clássico.

Sintoma:

autorização não funciona
CORS não funciona
tratamento de erro não captura tudo
logging ignora algumas requisições

Antes de adicionar mais código, desenhe o pipeline.

2. Criar try/catch em todos os endpoints

Isso duplica tratamento e produz respostas diferentes para o mesmo tipo de falha.

Prefira tratamento global para falhas inesperadas e tratamento local apenas quando existe uma decisão específica a tomar.

3. Guardar HttpContext para usar depois

Não trate HttpContext como singleton ou contexto global.

Extraia os dados necessários enquanto a requisição está válida.

4. Colocar regra de negócio em middleware

Middleware não deveria virar o lugar onde toda lógica "antes do endpoint" é colocada.

Use-o para preocupações transversais coerentes com o pipeline.

5. Logar request e response body indiscriminadamente

Pode gerar:

vazamento de segredo
LGPD e privacidade
custos de armazenamento
alto volume de logs
pressão de memória
exposição de tokens

Em aplicações de IA, bodies podem ser enormes.

Logging precisa de estratégia.

6. Acreditar que appsettings.json é a única configuração

Isso torna deployment rígido.

Use o sistema de configuration providers e mantenha configuração externa ao binário quando apropriado.

7. Depender de launchSettings.json em produção

Ele é ferramenta de desenvolvimento.

Não projete infraestrutura de produção ao redor dele.

8. Registrar singleton sem pensar em concorrência

Uma instância compartilhada pode receber múltiplos acessos concorrentes.

Thread safety deixa de ser opcional.

9. Retornar detalhes de exceção ao cliente

Stack trace é informação de diagnóstico interno.

Não transforme produção em debugger público.

10. Criar middleware pesado para toda requisição

Lembre-se:

middleware global
x
número de requisições

vira custo global.

Uma consulta a banco adicionada em um middleware executado em todas as rotas pode aumentar drasticamente latência e carga.

Performance: o pipeline também tem custo

ASP.NET Core é projetado para alto desempenho, mas nenhuma abstração é gratuita quando usada sem critério.

Considere:

app.Use(async (context, next) =>
{
    var body = await new StreamReader(
        context.Request.Body)
        .ReadToEndAsync();

    app.Logger.LogInformation(
        "Request body: {Body}",
        body);

    await next();
});

Parece um middleware de logging conveniente.

Mas agora pergunte:

Qual o tamanho máximo do body?

O body pode ser lido novamente?

Estamos carregando tudo em memória?

Existem arquivos de upload?

Existem dados sensíveis?

Qual o custo por requisição?

Uma solução aparentemente simples pode ser desastrosa em produção.

Imagine um upload de 500 MB.

Ou um request contendo um documento corporativo inteiro para processamento por IA.

O pipeline deve ser desenhado considerando streaming, limites e segurança.

Teremos aulas específicas sobre performance, segurança e upload de dados.

Como o pipeline se conecta com async e await

Observe que quase todo middleware moderno usa:

await next();

Isso não é acidente.

Servidores web passam grande parte do tempo esperando I/O:

banco de dados
HTTP externo
cache remoto
fila
storage
modelo de IA

ASP.NET Core é profundamente integrado ao modelo assíncrono do .NET.

Um endpoint futuro pode fazer:

app.MapPost(
    "/chat",
    async (
        ChatRequest request,
        IChatService chatService,
        CancellationToken cancellationToken
    ) =>
    {
        var response =
            await chatService.AskAsync(
                request.Message,
                cancellationToken);

        return Results.Ok(response);
    });

A palavra importante aqui não é apenas async.

É entender que enquanto esperamos um serviço externo, não queremos bloquear desnecessariamente recursos do servidor.

Teremos uma aula inteira sobre programação assíncrona.

Por enquanto, observe como ela já está embutida no design do pipeline.

Como isso se conecta com aplicações de IA

Imagine nossa futura arquitetura:

User
  |
  v
ASP.NET Core
  |
  +--> Correlation
  +--> Rate Limiting
  +--> Authentication
  +--> Authorization
  +--> Logging
  |
  v
/chat endpoint
  |
  v
AI Orchestrator
  |
  +--> LLM
  +--> Vector Search
  +--> Tools

Todos os fundamentos desta aula continuam existindo.

Kestrel

Receberá as conexões dos clientes ou da infraestrutura de borda.

Middleware

Poderá implementar preocupações como:

correlation
rate limiting
security headers
observability
exception handling

Routing

Mapeará endpoints como:

POST /chat
POST /documents
GET /conversations/{id}

Dependency Injection

Fornecerá componentes como:

IChatClient
IVectorStore
IDocumentRepository
IEmbeddingGenerator

Configuration

Definirá:

model deployment
feature settings
timeouts
limits
provider endpoints

Logging

Precisará registrar o suficiente para diagnosticar problemas sem vazar dados sensíveis.

Error handling

Precisará transformar falhas de:

LLM timeout
rate limit
vector database unavailable
tool failure

em respostas coerentes.

A inteligência artificial não substitui o pipeline HTTP.

Ela entra dentro dele.

Um pequeno desafio: descubra a ordem

Considere:

app.Use(async (context, next) =>
{
    Console.WriteLine("A1");
    await next();
    Console.WriteLine("A2");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("B1");

    if (context.Request.Path == "/stop")
    {
        context.Response.StatusCode = 403;
        return;
    }

    await next();

    Console.WriteLine("B2");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("C1");
    await next();
    Console.WriteLine("C2");
});

app.MapGet("/go", () =>
{
    Console.WriteLine("ENDPOINT");
    return Results.Ok();
});

Para /go, a ordem será conceitualmente:

A1
B1
C1
ENDPOINT
C2
B2
A2

Agora para /stop:

A1
B1
A2

Por quê?

B não chamou next().

Logo:

C
endpoint
B2

não executam.

Mas A2 executa porque A estava aguardando B, e B retornou normalmente.

Se você entende esse exercício sem executar o código, começou a dominar o modelo mental do pipeline.

Exercício completo da aula

Crie um projeto:

Logby.Store.Api

Implemente os seguintes componentes.

1. Endpoint de health

GET /health

Resposta:

{
  "status": "Healthy"
}

2. Endpoint para listar pedidos

GET /orders

Use um armazenamento em memória.

3. Endpoint para buscar pedido

GET /orders/{id}

Use constraint de GUID.

Retorne 404 quando não encontrar.

4. Correlation middleware

Leia:

X-Correlation-Id

Se não existir, gere um GUID.

Devolva o mesmo valor na resposta.

5. Request timing middleware

Registre:

method
path
status code
elapsed time

Use logging estruturado.

6. Configuração tipada

Crie:

{
  "Store": {
    "Currency": "USD",
    "MaxOrdersPerRequest": 100
  }
}

Use Options Pattern e valide na inicialização.

7. Problem Details

Configure tratamento global de erros.

Crie temporariamente um endpoint:

GET /debug/error

que lança uma exceção para você observar o comportamento.

Depois remova esse endpoint antes de considerar o exercício concluído.

8. Experimento de short-circuit

Adicione um middleware temporário que retorna 403 para:

/blocked

Coloque logs antes e depois de next() nos demais middlewares e observe quais componentes executam.

O objetivo não é apenas ter uma API funcionando.

É conseguir prever a execução antes de fazer a requisição.

Checklist antes de seguir

Antes da próxima aula, você deve conseguir explicar:

  1. qual é o papel do ASP.NET Core;
  2. qual é o papel do Kestrel;
  3. o que WebApplication.CreateBuilder prepara;
  4. a diferença conceitual entre configurar serviços e configurar o pipeline;
  5. o que é HttpContext;
  6. como uma requisição atravessa middleware;
  7. por que código depois de await next() executa no caminho de retorno;
  8. o que significa short-circuit;
  9. por que a ordem de middleware importa;
  10. a diferença conceitual entre Use, Run e ramificação do pipeline;
  11. o papel do routing;
  12. o que é um endpoint;
  13. por que endpoint não significa necessariamente controller;
  14. como configuração pode vir de múltiplas fontes;
  15. por que Options Pattern melhora configurações relacionadas;
  16. por que segredo não deveria ficar no appsettings.json versionado;
  17. o que são ambientes como Development e Production;
  18. por que tratamento global de erros é melhor que repetir try/catch em todo endpoint;
  19. por que singleton exige pensar em concorrência;
  20. como esses fundamentos serão reutilizados em aplicações de IA.

Se esses pontos estiverem claros, a próxima etapa ficará muito mais fácil.

Guia rápido de decisão

Use middleware quando

A preocupação é transversal ao fluxo HTTP e precisa envolver muitos endpoints.

Exemplos:

exception handling
correlation
logging HTTP
headers
security
compression
authentication pipeline

Não use middleware quando

A lógica é uma regra de negócio específica de uma funcionalidade.

Exemplos:

calcular desconto de pedido
validar estoque
aprovar pagamento
definir política comercial

Use configuração externa quando

O valor muda entre ambientes ou deployments.

Exemplos:

URLs de serviços
limites
feature settings
connection configuration
timeouts

Use Options Pattern quando

Existe um conjunto coerente de configurações que merece um contrato tipado e validação.

Use environment para

Descrever o contexto operacional da aplicação.

Exemplos:

Development
Staging
Production

Não use environment como substituto de feature flags.

Use logging estruturado quando

Praticamente sempre que você estiver registrando eventos operacionais que precisarão ser pesquisados ou correlacionados.

Prefira:

logger.LogInformation(
    "Order {OrderId} processed",
    orderId);

em vez de:

logger.LogInformation(
    $"Order {orderId} processed");

O primeiro preserva a propriedade OrderId como parte da estrutura lógica do evento.

Conclusão

Uma aplicação ASP.NET Core não é apenas um conjunto de endpoints.

Ela é um pipeline.

Quando uma requisição chega, Kestrel recebe a conexão e entrega o trabalho à infraestrutura do ASP.NET Core.

A requisição ganha um contexto.

Esse contexto atravessa uma cadeia de middleware.

Cada componente pode observar, modificar, interromper ou encaminhar a execução.

Routing encontra o endpoint apropriado.

Dependências são resolvidas.

O código da aplicação executa.

A resposta atravessa novamente o pipeline no caminho de volta até chegar ao cliente.

Ao mesmo tempo, hosting, configuração, ambientes, logging e tratamento de erros sustentam a aplicação durante todo o seu ciclo de vida.

Esse conhecimento muda completamente a forma como enxergamos uma API.

Em vez de pensar:

URL -> método

passamos a pensar:

Connection
   |
   v
Kestrel
   |
   v
HttpContext
   |
   v
Middleware pipeline
   |
   v
Routing
   |
   v
Endpoint
   |
   v
Application
   |
   v
Response pipeline

Esse é o alicerce para tudo que vem a seguir.

Na próxima aula vamos focar em Web APIs.

Vamos sair da infraestrutura geral do ASP.NET Core e estudar como projetar contratos HTTP de verdade: métodos HTTP, status codes, recursos, DTOs, model binding, validação, idempotência, paginação, Problem Details, versionamento, cancelamento e decisões que transformam um endpoint funcional em uma API que consegue evoluir sem quebrar seus consumidores.

Fontes

← voltar ao índice