Middleware no ASP.NET: o que é e como criar o seu

O middleware no ASP.NET Core é um dos conceitos mais fundamentais do framework e, ao mesmo tempo, um dos que mais geram comportamentos inesperados em produção. A maior parte desses problemas não vem de bugs, e sim de suposições erradas sobre quando cada componente é construído, quando executa e por quanto tempo seus objetos permanecem vivos.

Neste artigo, você vai entender o funcionamento do pipeline de requisições, escrever um middleware próprio e analisar quatro armadilhas recorrentes. São elas: esquecer de chamar next, injetar serviços de escopo no construtor, alterar a resposta depois que ela começou e reter serviços de escopo após o fim da requisição.

Todos os exemplos consideram o .NET 10, versão de suporte de longo prazo (LTS) disponível desde novembro de 2025. Segundo a política oficial, essa versão recebe suporte até novembro de 2028.

O que é middleware no ASP.NET

Middleware é um software montado em um pipeline da aplicação para tratar requisições e respostas. Cada componente decide se passa a requisição adiante e pode executar trabalho antes e depois do componente seguinte.

Essa estrutura nasce a partir de delegates de requisição, representados pelo tipo RequestDelegate. Cada delegate recebe o HttpContext da requisição corrente e devolve uma Task.

O ponto essencial é que o pipeline não é uma lista de handlers independentes, e sim uma cadeia aninhada. Ou seja, o primeiro middleware registrado envolve o segundo, que envolve o terceiro, e assim por diante até o endpoint final.

A consequência prática é que existem dois momentos distintos de execução dentro do mesmo método. O código antes da chamada a next roda na entrada, enquanto o código depois dela roda no retorno, quando a resposta já sobe de volta pela cadeia.

Use, Run e Map no middleware do ASP.NET

Para configurar um middleware no ASP.NET, o framework expõe três métodos de extensão principais, chamados Use, Run e Map. O Use adiciona um componente que pode continuar o pipeline, e o Run adiciona um componente terminal.

Já o Map cria ramificações condicionais baseadas no caminho da requisição. Dessa forma, um trecho do pipeline atende apenas rotas que começam com determinado prefixo.

Quando um middleware decide não invocar o próximo componente, dizemos que ele fez um curto-circuito no pipeline. Esse comportamento é legítimo em cenários como autenticação, cache e limitação de taxa, mas se torna um defeito grave quando acontece por acidente.

Como criar um middleware no ASP.NET

A forma mais direta de adicionar um middleware no ASP.NET é usar app.Use no Program.cs, com uma expressão lambda.

// Program.cs
app.Use(async (context, next) =>
{
    Console.WriteLine($"Request: {context.Request.Path}");
    await next(context);
    Console.WriteLine($"Response: {context.Response.StatusCode}");
});

Esse componente executa em toda requisição HTTP que chega à aplicação. A primeira linha roda na entrada, a chamada a next transfere o controle para o restante do pipeline e a última linha roda quando o controle retorna.

A abordagem funciona, mas tem limitações claras de manutenção. Afinal, a lógica fica acoplada ao arquivo de inicialização, não pode ser reaproveitada e não existe um tipo para instanciar em um teste de unidade.

Movendo o middleware no ASP.NET para uma classe

O padrão recomendado pela documentação oficial é o middleware baseado em convenção. Trata-se de uma classe comum, sem interface obrigatória, que segue duas regras de assinatura.

// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;

    public RequestLoggingMiddleware(RequestDelegate next) =>
        _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        Console.WriteLine($"Request: {context.Request.Path}");
        await _next(context);
        Console.WriteLine($"Response: {context.Response.StatusCode}");
    }
}

A primeira regra é que o construtor precisa receber um RequestDelegate, que representa o próximo componente da cadeia. A segunda é que a classe precisa expor um método público Invoke ou InvokeAsync que receba o HttpContext e devolva uma Task.

Em seguida, registramos o componente com UseMiddleware, informando o tipo como parâmetro genérico.

// Program.cs
app.UseMiddleware<RequestLoggingMiddleware>();

Sem esse registro, o middleware no ASP.NET nunca executa. Não existe descoberta automática por convenção de nome ou por varredura de assembly, o que costuma surpreender quem vem de frameworks com registro implícito.

Criando um método de extensão

É comum encapsular a chamada em um método de extensão sobre IApplicationBuilder. Assim, o Program.cs fica mais legível e o middleware pode receber parâmetros de configuração no futuro.

// RequestLoggingMiddlewareExtensions.cs
public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(
        this IApplicationBuilder builder) =>
        builder.UseMiddleware<RequestLoggingMiddleware>();
}

Com isso, o registro passa a ser apenas app.UseRequestLogging(), no mesmo estilo dos componentes embutidos do framework. Se quiser revisar o conceito, veja o artigo sobre extension methods no C#.

A ordem do middleware no ASP.NET define o comportamento

A ordem em que os componentes são registrados determina literalmente a ordem de execução do pipeline, tanto na entrada quanto na saída. Por isso, registrar um middleware primeiro significa que ele verá a requisição antes de todos e escreverá o log de resposta depois de todos.

// Program.cs
app.UseMiddleware<RequestLoggingMiddleware>(); // executa primeiro

app.MapMinimalApiEndpoints();

if (app.Environment.IsDevelopment())
{
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/openapi/v1.json", "Api");
    });
    app.MapOpenApi();
}

A documentação de middleware no ASP.NET Core define uma sequência de referência para os componentes embutidos, e essa sequência não é arbitrária. O tratamento de exceções vem primeiro para capturar falhas de tudo o que está abaixo, e os arquivos estáticos vêm cedo para evitar processamento desnecessário.

Além disso, UseCors, UseAuthentication e UseAuthorization precisam aparecer nessa ordem. A autorização depende do HttpContext.User já preenchido pela autenticação.

O middleware de antiforgery, por sua vez, precisa vir depois de UseAuthentication e UseAuthorization. Inverter qualquer um desses pares não gera erro de compilação e, frequentemente, nem erro em tempo de execução, apenas um comportamento silenciosamente incorreto.

Para o tratamento de erros, vale conhecer o IExceptionHandler no .NET 10, que roda dentro do middleware de exceções. Da mesma forma, o rate limiting no ASP.NET Core 10 tem regras próprias de posicionamento em relação ao roteamento.

Armadilha 1: esquecer de chamar next

A primeira armadilha do middleware no ASP.NET é também a mais direta. Se o middleware não chama next, nada abaixo dele executa.

public async Task InvokeAsync(HttpContext context)
{
    Console.WriteLine($"Request: {context.Request.Path}");
    //await _next(context);
    Console.WriteLine($"Response: {context.Response.StatusCode}");
}

Com o RequestLoggingMiddleware registrado como primeiro componente, comentar a chamada a _next derruba todo o restante do pipeline. Os endpoints da Minimal API, a interface do Swagger e qualquer outro middleware registrado depois deixam de existir para a requisição.

O sintoma é traiçoeiro porque não há exceção. O cliente recebe uma resposta HTTP 200 com corpo vazio, já que o status padrão do HttpResponse é 200 e ninguém escreveu nada nele.

Em um navegador, a página simplesmente não carrega. Já em uma ferramenta como o Postman, o resultado confunde ainda mais, porque um 200 sem conteúdo passa a impressão de que a aplicação respondeu corretamente.

Vale reforçar que existe um caminho legítimo para esse comportamento. Quando a intenção é realmente encerrar o pipeline, o método correto é Run, que a própria documentação indica para delegates que não chamam a função seguinte.

Armadilha 2: o middleware no ASP.NET é singleton

A segunda armadilha envolve o ciclo de vida dos objetos. O middleware no ASP.NET baseado em convenção é construído uma única vez, no início da aplicação, e a mesma instância atende todas as requisições.

Isso significa que o construtor resolve as dependências a partir do provedor raiz de serviços, e não de um escopo de requisição. Portanto, injetar um serviço Scoped nesse construtor faria esse serviço se comportar como um singleton, vivo pela aplicação inteira.

// Program.cs
builder.Services.AddScoped<IMyScopedService, MyScopedService>();

Ao injetar IMyScopedService no construtor do middleware e executar a aplicação, o resultado é uma exceção de inicialização.

Cannot resolve scoped service 'IMyScopedService' from root provider

Essa mensagem vem da validação de escopo do contêiner de injeção de dependência, habilitada por padrão no ambiente de desenvolvimento. Na prática, a validação transforma um vazamento silencioso de ciclo de vida em uma falha explícita e imediata.

Se os ciclos de vida ainda geram dúvida, o artigo sobre AddTransient, AddScoped e AddSingleton detalha cada um deles.

Injetando no InvokeAsync

A solução recomendada pela documentação é mover a dependência para a assinatura do InvokeAsync. Esse método aceita parâmetros adicionais que o contêiner popula a cada requisição.

public async Task InvokeAsync(
    HttpContext context,
    IMyScopedService myScopedService)
{
    Console.WriteLine($"Request: {context.Request.Path}");
    await _next(context);
    Console.WriteLine($"Response: {context.Response.StatusCode}");
}

Como o InvokeAsync roda uma vez por requisição, o contêiner resolve o serviço a partir do escopo daquela requisição específica. Assim, temos uma instância por requisição, compartilhada com os demais tipos do mesmo escopo.

A alternativa: ativação baseada em fábrica

Quando a quantidade de dependências de escopo cresce, a assinatura do InvokeAsync começa a ficar poluída. Para esse cenário, o framework oferece um segundo modelo de ativação, baseado nas interfaces IMiddleware e IMiddlewareFactory.

Um middleware no ASP.NET que implementa IMiddleware é ativado por requisição, o que permite injetar serviços de escopo no construtor. O UseMiddleware detecta a interface e delega a criação da instância à fábrica registrada no contêiner.

// ScopedLoggingMiddleware.cs
public class ScopedLoggingMiddleware(IMyScopedService myScopedService)
    : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // o serviço de escopo chega pelo construtor
        await next(context);
    }
}

// Program.cs
builder.Services.AddScoped<ScopedLoggingMiddleware>();
app.UseMiddleware<ScopedLoggingMiddleware>();

O custo dessa abordagem é registrar o próprio middleware no contêiner, como serviço Scoped ou Transient. Em troca, ganhamos tipagem forte, um contrato explícito e um modelo de dependências muito mais previsível.

Armadilha 3: alterar a resposta depois que ela começou

A terceira armadilha aparece no trecho que executa depois da chamada a next. Nesse ponto, o desenvolvedor tenta modificar uma resposta que já retornou do pipeline.

public async Task InvokeAsync(HttpContext context)
{
    await _next(context);

    context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
    await context.Response.WriteAsync("<p>This page is unavailable</p>");
}

O resultado é uma exceção clara em tempo de execução.

System.InvalidOperationException: StatusCode cannot be set because the response has already started.

A explicação está no conceito de resposta iniciada, exposto pela propriedade HttpResponse.HasStarted. Quando ela vale true, os cabeçalhos já seguiram para o cliente, e status, motivo e cabeçalhos se tornam imutáveis.

Isso é uma consequência do próprio protocolo HTTP, e não uma restrição do framework. Depois que os bytes do cabeçalho chegam ao socket, não existe como reescrevê-los.

Usando o OnStarting

O ponto de extensão adequado para agir imediatamente antes desse envio é o HttpResponse.OnStarting. Ele registra um callback que executa logo antes de a resposta começar.

context.Response.OnStarting(() =>
{
    if (!context.Response.HasStarted && MaintenancePage)
    {
        context.Response.StatusCode =
            StatusCodes.Status503ServiceUnavailable;
        context.Response.Headers.RetryAfter = "120";
    }

    return Task.CompletedTask;
});

await _next(context);

É importante delimitar bem o escopo desse recurso. O OnStarting serve para ajustar status e cabeçalhos na última janela possível, e não para escrever corpo de resposta.

Gravar no corpo dentro do callback é arriscado, porque nesse momento os cabeçalhos já estão prontos para envio. Em respostas com Content-Length definido, a escrita adicional gera divergência entre o tamanho declarado e o tamanho real.

Para uma página de manutenção, o desenho mais previsível é decidir antes de chamar next. Assim, o middleware verifica a condição, escreve a resposta e não continua o pipeline, o que evita o problema por completo.

Armadilha 4: reter serviços de escopo depois da resposta

A quarta armadilha do middleware no ASP.NET é a mais difícil de diagnosticar, porque a aplicação continua funcionando aparentemente bem. Ela ocorre quando um callback em HttpResponse.OnCompleted dispara trabalho em segundo plano com um serviço de escopo recebido no InvokeAsync.

context.Response.OnCompleted(() =>
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(TimeSpan.FromSeconds(5));
        myScopedService.Test(); // serviço já descartado
    });

    return Task.CompletedTask;
});

O OnCompleted roda após o envio completo da resposta, e a documentação da API indica que ele se destina à limpeza de recursos. Quando ele executa, o escopo da requisição já terminou, e todos os serviços descartáveis daquele escopo já receberam a chamada de Dispose.

O sintoma inicial engana, porque a página continua renderizando corretamente e o log parece indicar sucesso. A falha só aparece quando o serviço passa a validar o próprio estado.

public void Test()
{
    ObjectDisposedException.ThrowIf(Disposed, this);
    _logger.LogInformation("MyScopedService.Test() executed");
}

A partir daí, os logs passam a registrar a exceção de acesso a objeto descartado, enquanto a resposta HTTP segue correta. Justamente esse descolamento entre o que o usuário vê e o que o sistema registra torna o problema perigoso.

Criando um escopo próprio

A correção consiste em não reaproveitar o escopo da requisição para trabalho que sobrevive a ela. Com IServiceScopeFactory injetado no middleware, o trabalho em segundo plano cria o próprio escopo e resolve o serviço a partir dele.

context.Response.OnCompleted(() =>
{
    _ = Task.Run(async () =>
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var service = scope.ServiceProvider
            .GetRequiredService<IMyScopedService>();

        await Task.Delay(TimeSpan.FromSeconds(5));
        service.Test();
    });

    return Task.CompletedTask;
});

Com o escopo próprio, o tempo de vida do serviço acompanha o trabalho em segundo plano, e não mais a requisição original. Além disso, o using garante o descarte determinístico assim que a operação termina.

Existe, porém, uma ressalva que vai além da injeção de dependência. Trabalho disparado com Task.Run e sem acompanhamento não tem garantia de conclusão, especialmente durante o desligamento da aplicação.

Para tarefas que realmente importam, o caminho mais robusto é publicar a mensagem em uma fila e processá-la em um serviço hospedado. Nesse modelo, o middleware apenas enfileira, e o host gerencia o ciclo de vida da execução, como mostra o artigo sobre background services em C# e .NET.

Boas práticas para criar o seu middleware no ASP.NET

Antes de escrever um middleware no ASP.NET, verifique se o comportamento desejado não pertence a outra camada. Filtros de endpoint, filtros de ação e o próprio roteamento resolvem muitos cenários com acesso a metadados que o middleware não tem.

Quando o middleware for realmente a escolha certa, algumas diretrizes reduzem bastante o risco das armadilhas descritas. Primeiro, sempre chame next, a menos que a intenção explícita seja encerrar o pipeline, e nesse caso prefira Run.

Em seguida, mantenha o construtor restrito a dependências singleton, como ILogger e opções de configuração. Reserve o InvokeAsync para dependências de escopo, ou adote IMiddleware quando o volume delas justificar.

Também evite qualquer alteração de status ou cabeçalho depois da chamada a next, e verifique HasStarted sempre que houver dúvida. Decisões que afetam a resposta devem acontecer o mais cedo possível no pipeline.

Por fim, trate cada middleware no ASP.NET como código de produção sujeito a testes. A classe isolada aceita um DefaultHttpContext e um RequestDelegate falso em um teste de unidade, o que permite verificar o caminho feliz e o curto-circuito.

Conclusão

O pipeline de middleware no ASP.NET Core é simples na superfície e exige atenção nos detalhes. As quatro armadilhas discutidas compartilham a mesma raiz: a diferença entre o ciclo de vida que o desenvolvedor imagina e o que o framework realmente aplica.

O componente nasce uma vez, mas executa milhares de vezes. A resposta aceita modificações, mas apenas até o instante em que os cabeçalhos seguem para o cliente.

Já o escopo da requisição existe, mas termina antes de o trabalho em segundo plano começar. Compreender esses três limites resolve a grande maioria dos problemas, e o restante é disciplina de ordenação e de testes.

FAQ

Qual a diferença entre Use e Run?

O Use adiciona um middleware que pode invocar o próximo componente, enquanto o Run adiciona um delegate terminal que encerra a cadeia. Se o seu componente não vai chamar next, a documentação recomenda Run.

Por que o meu middleware não executa?

Na maioria dos casos, ele não foi registrado com UseMiddleware ou está depois de um componente que fez curto-circuito. Não existe registro automático por convenção de nome.

Posso injetar um DbContext no construtor do middleware?

Não, se ele estiver registrado como Scoped, que é o padrão de AddDbContext. Injete-o no InvokeAsync ou use a ativação baseada em IMiddleware.

Qual a diferença entre OnStarting e OnCompleted?

O OnStarting executa imediatamente antes do envio da resposta, sendo o último ponto para alterar status e cabeçalhos. O OnCompleted executa após o envio completo e serve para limpeza de recursos.

Como retornar uma página de manutenção sem quebrar a resposta?

Verifique a condição antes de chamar next, escreva a resposta e não continue o pipeline. Isso evita a exceção de resposta já iniciada.

Middleware baseado em convenção ou em fábrica?

Use convenção como padrão, por ser mais leve e não exigir registro no contêiner. Migre para IMiddleware quando precisar de várias dependências de escopo ou de um contrato tipado.

Como testar um middleware?

Instancie a classe com um RequestDelegate controlado por você e chame InvokeAsync com um DefaultHttpContext. Assim, você verifica se o próximo componente foi chamado e quais cabeçalhos foram escritos.

Qual versão do .NET devo usar hoje?

O .NET 10 é a versão LTS atual, disponível desde novembro de 2025 e com suporte até novembro de 2028. O .NET 8 e o .NET 9 encerram o suporte em 10 de novembro de 2026.