ShortCircuit: Pulando o pipeline de Middlewares no ASP.NET Core

Você já parou para pensar em quantos Middlewares são executados antes da sua Controller responder um simples “OK” ? Autenticação, CORS, Roteamento, Rate Limiting, Logging… e às vezes o seu endpoint nem precisa de nada disso!

Pensando nisso, o ASP.NET Core adicionou um novo atributo chamado [ShortCircuit], que faz o endpoint ser executado logo após o Roteamento, “pulando” todo o restante do pipeline de Middlewares.

Mas isto não é novidade ?

Na verdade não é 100% novo. Já existia o conceito de ShortCircuit() como uma convenção de endpoint, usada principalmente em Minimal APIs. A novidade aqui é que agora temos a versão em formato de atributo, podendo ser aplicado diretamente em Controllers e Actions do MVC, deixando o recurso muito mais simples de usar no dia a dia de quem ainda trabalha com Controllers.

Por que usar o Short-Circuit ?

Pense em cenários como:

  • Um endpoint de health check, que só precisa responder “estou vivo”;
  • Um robots.txt, que é um arquivo estático de texto;
  • Qualquer endpoint que não dependa de autenticação, CORS ou outros Middlewares customizados da sua aplicação.

Nestes casos, todo o processamento dos Middlewares anteriores é um custo desnecessário. O [ShortCircuit] evita essa sobrecarga, pois o endpoint já responde assim que o Roteamento identifica qual Controller/Action deve ser chamada.

Importante: o endpoint continua sendo executado normalmente e produzindo a sua resposta, o que muda é que os Middlewares que estariam entre o Roteamento e a execução da Action não rodam.

Usando no Controller inteiro

Você pode aplicar o atributo na classe, fazendo com que todas as Actions daquela Controller sejam short-circuited:

[ApiController]
[Route("[controller]")]
[ShortCircuit]
public class HomeController : ControllerBase
{
    public IActionResult Get() => Ok("ShortCircuit");
}

Usando apenas em um método

Se você quer aplicar o comportamento somente em uma Action específica, basta colocar o atributo no método, e não na classe:

[ApiController]
[Route("[controller]")]
public class Home3Controller : ControllerBase
{
    [ShortCircuit]
    public IActionResult Get() => Ok("ShortCircuit no Methodo");
}

E para efeito de comparação, uma Controller “normal”, sem o atributo, que vai passar por todo o pipeline de Middlewares normalmente:

[ApiController]
[Route("[controller]")]
public class Home2Controller : ControllerBase
{
    public IActionResult Get() => Ok("Sem ShortCircuit");
}

Exemplo real: robots.txt

Um dos casos mais clássicos de uso é justamente o robots.txt. Ele não precisa de autenticação, não precisa de CORS, é só um texto estático que os robôs de busca vão ler:

[ApiController]
[Route("robots.txt")]
[ShortCircuit]
public class RobotsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Content("User-agent: *\nDisallow:", "text/plain");
}

Repare que o [Route("robots.txt")] já cuida do endereço, e o [ShortCircuit] garante que essa resposta seja a mais rápida possível, sem passar pelos demais Middlewares configurados no Program.cs.

Definindo um status code diferente

Você também pode passar um status code opcional para o atributo, por exemplo se quiser retornar um 404 diretamente:

[ShortCircuit(404)]

Comprovando o Short-Circuit com um Middleware

Para deixar mais claro o efeito do [ShortCircuit], adicionei ao projeto um Middleware chamado ExceptionHandlingMiddleware. Ele foi colocado ali apenas para fins didáticos, para mostrarmos quando o pipeline de Middlewares realmente é executado:

using System.Net;
using System.Text.Json;

namespace CircuitoCurto.Middlewares;

internal sealed class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
            Console.WriteLine($"{DateTime.Now} - Request para {context.Request.Path}");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error {context.Request.Path}");
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var statusCode = exception switch
        {
            ArgumentException => HttpStatusCode.BadRequest,
            KeyNotFoundException => HttpStatusCode.NotFound,
            UnauthorizedAccessException => HttpStatusCode.Unauthorized,
            NotImplementedException => HttpStatusCode.NotImplemented,
            _ => HttpStatusCode.InternalServerError
        };

        var payload = new
        {
            status = (int)statusCode,
            error = statusCode.ToString(),
            message = exception.Message
        };

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)statusCode;
        return context.Response.WriteAsync(JsonSerializer.Serialize(payload));
    }
}

Repare no Console.WriteLine($"{DateTime.Now} - Request para {context.Request.Path}");. É essa linha que vai denunciar se o Middleware foi executado ou não.

Como este Middleware é registrado no Program.cs com app.UseMiddleware<ExceptionHandlingMiddleware>(), ele faz parte do pipeline “normal” da aplicação. Então, se chamarmos:

  • /Home2 (sem o [ShortCircuit]): o texto aparece no Console, pois a Request passou pelo Middleware normalmente;
  • /Home ou /Home3 (com o [ShortCircuit]): o texto não aparece no Console, comprovando que o Middleware foi mesmo pulado!

É uma forma simples e visual de comprovar que o atributo realmente está fazendo o que promete.

Considerações

É um recurso simples, mas que ajuda bastante em performance quando você tem endpoints que realmente não precisam de todo aquele pipeline de Middlewares rodando por baixo dos panos. Health checks e arquivos estáticos como o robots.txt são os candidatos perfeitos para o [ShortCircuit].

Vale lembrar: use com cuidado! Se o seu endpoint short-circuited passar a precisar de autenticação ou CORS no futuro, você vai precisar remover o atributo, senão aquele Middleware simplesmente não vai ser executado.

Um detalhe importante é que esta funcionalidade está disponivel no .NET 11 Preview 6.

O código fonte do exemplo está no meu GitHub.

Abraços e até a próxima!