A maior e melhor Black Friday que já fizemos! Descontos a partir de 50% em até 12x sem juros - Garanta seu Desconto

Controllers no .NET 11: ShortCircuit, unions e validação

Controllers no .NET 11: ShortCircuit, unions e validação

Os controllers no .NET 11 finalmente recebem recursos novos, depois de vários ciclos em que quase toda a evolução do ASP.NET Core ficou concentrada nas Minimal APIs. A versão chegou em novembro de 2026 com o C# 15, e traz o atributo [ShortCircuit], suporte a union types e outras melhorias que valem para quem mantém APIs baseadas em MVC.

Ao mesmo tempo, alguns recursos continuam exclusivos das Minimal APIs. Por isso, este guia mostra o que mudou, o que ainda falta e como decidir se vale migrar agora.

Por que os controllers no .NET 11 merecem atenção

Desde o .NET 6, a Microsoft direciona boa parte do trabalho do ASP.NET Core para as Minimal APIs. Além disso, a documentação oficial passou a recomendar esse modelo para novas Web APIs.

No entanto, milhares de aplicações em produção ainda usam ControllerBase, filtros MVC e roteamento por atributos. Para esses times, os controllers no .NET 11 trazem ganhos concretos sem exigir uma reescrita.

As novidades se dividem em três grupos. Primeiro, recursos que chegam aos controllers depois de existirem nas Minimal APIs, como o short circuit.

Em seguida, recursos novos da plataforma que funcionam nos dois modelos, como os union types do C# 15. Por fim, recursos que continuam exclusivos das Minimal APIs, como a validação assíncrona.

Além desses três grupos, o .NET 11 traz duas mudanças menores que também alcançam o MVC. Os atributos que implementam IAuthorizationRequirementData agora são aplicados em controllers e actions, e não apenas em endpoints de Minimal APIs.

Da mesma forma, o ApiExplorer passou a preservar todos os [ProducesResponseType] declarados para o mesmo status code. Antes, o framework mantinha só um tipo por status e descartava os demais sem aviso.

ShortCircuit nos controllers no .NET 11

Toda requisição que chega a um controller atravessa o pipeline de middlewares: autenticação, autorização, CORS, rate limiting, logging e o que mais a aplicação registrar. Na maioria dos endpoints, isso é exatamente o desejado.

Contudo, alguns endpoints não precisam de nada disso. Um health check, um robots.txt ou um endpoint de prontidão para o orquestrador só precisam responder rápido.

O .NET 8 já resolvia esse problema nas Minimal APIs com o método ShortCircuit(). Agora, os controllers no .NET 11 ganham a versão em atributo, [ShortCircuit], que fica no namespace Microsoft.AspNetCore.Routing.

Diagrama comparando o pipeline dos controllers no .NET 11 com e sem o atributo ShortCircuit, em que o endpoint marcado é executado logo após o UseRouting
Com o atributo, o endpoint roda dentro do middleware de roteamento e ignora os middlewares registrados depois dele.

Como o atributo funciona na prática

O atributo pode ser aplicado na classe do controller ou em uma action específica. Quando ele está na classe, todas as actions do controller passam a ignorar o restante do pipeline.

// HealthController.cs
[ApiController]
[Route("api/health")]
[ShortCircuit]
public class HealthController(HealthCheckService healthCheckService) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> Get(CancellationToken cancellationToken)
    {
        // Executa todos os health checks registrados
        var report = await healthCheckService.CheckHealthAsync(cancellationToken);

        var result = new
        {
            Status = report.Status.ToString(),
            Checks = report.Entries.Select(entry => new
            {
                Name = entry.Key,
                Status = entry.Value.Status.ToString()
            })
        };

        return report.Status == HealthStatus.Unhealthy
            ? StatusCode(StatusCodes.Status503ServiceUnavailable, result)
            : Ok(result);
    }
}

Para comprovar o efeito, basta registrar um middleware simples no Program.cs e observar o log.

// Program.cs
app.Use(async (context, next) =>
{
    var logger = context.RequestServices
        .GetRequiredService<ILoggerFactory>()
        .CreateLogger("Pipeline");

    // Esta linha não aparece quando o endpoint usa ShortCircuit
    logger.LogInformation("Middleware ran");

    await next(context);
});

Sem o atributo, a chamada ao Get sempre passa pelo middleware e a mensagem aparece no log. Com o atributo, o middleware não executa, e mesmo assim o endpoint devolve a resposta normalmente.

Na prática, o endpoint passa a rodar dentro do próprio middleware de roteamento. Dessa forma, tudo o que foi registrado depois do UseRouting fica de fora daquela requisição.

Status code padrão com ShortCircuit

O atributo aceita um status code opcional. Ele vale para actions que não definem o status por conta própria, como métodos void.

// HealthController.cs
[ApiController]
[Route("api/health")]
[ShortCircuit(204)]
public class ReadinessController : ControllerBase
{
    [HttpGet("app-ready")]
    public void AppReady()
    {
        // Nenhum retorno: a resposta usa o status 204 do atributo
    }
}

Como o método AppReady não devolve nada, a resposta sai com o status 204. Por outro lado, uma action que retorna Ok() continua respondendo 200, porque o status explícito prevalece.

Benefícios e restrições do ShortCircuit nos controllers no .NET 11

Pular parte do pipeline traz três benefícios diretos para os controllers no .NET 11:

  • menos CPU e memória por requisição;
  • nenhum middleware consegue interferir na resposta;
  • health checks deixam de poluir os logs de requisição e a telemetria.

Entretanto, o recurso tem limites claros. Middlewares registrados antes do UseRouting continuam rodando, porque o atalho só acontece depois que a rota é resolvida.

Além disso, um endpoint com short circuit não pode ter metadados de autorização ou de CORS. Se a action tiver [Authorize] ou [EnableCors], a requisição falha com InvalidOperationException.

Em outras palavras, o ShortCircuit nos controllers no .NET 11 serve para endpoints públicos e simples. Ele não substitui a segurança da API, e nunca deve aparecer em uma rota que precise de autenticação.

Union types nos controllers no .NET 11

O .NET 11 traz o C# 15 como versão padrão da linguagem, e o C# 15 introduz os union types. Uma union representa um valor que pertence a exatamente um tipo de um conjunto fechado.

Nos controllers no .NET 11, esse recurso resolve um problema antigo: retornar formatos diferentes no mesmo endpoint sem herança ou object. Por isso, o ASP.NET Core passou a aceitar unions em todo lugar onde usa o System.Text.Json.

Como declarar uma union no C# 15

Considere uma API de pagamentos que devolve um pagamento com cartão ou uma transferência bancária. Primeiro, cada formato vira um record próprio.

// CardPaymentDto.cs
public record CardPaymentDto(string Last4, decimal Amount);

// BankTransferDto.cs
public record BankTransferDto(string Reference, decimal Amount);

Em seguida, a palavra-chave union declara o tipo que os controllers no .NET 11 vão retornar.

// PaymentDto.cs
public union PaymentDto(CardPaymentDto, BankTransferDto);

A union gera conversões implícitas a partir de cada caso. Assim, a action pode atribuir qualquer um dos records diretamente a uma variável do tipo PaymentDto.

// PaymentsController.cs
[ApiController]
[Route("api/payments")]
public class PaymentsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public ActionResult<PaymentDto> Get(int id)
    {
        // A conversão implícita transforma cada record em PaymentDto
        PaymentDto payment = id == 1
            ? new CardPaymentDto("4242", 49.99m)
            : new BankTransferDto("RTC-1001", 120.00m);

        return payment;
    }
}

Além disso, o compilador exige que um switch sobre a union trate todos os casos. Quando alguém adicionar um terceiro tipo de pagamento, cada switch incompleto vai gerar um aviso.

Há também uma limitação importante. Unions funcionam no corpo JSON da requisição e da resposta, mas não em route values, query strings, headers ou campos de formulário.

Union types na documentação OpenAPI

Segundo a documentação oficial, um endpoint que retorna uma union aparece no documento OpenAPI com um schema anyOf, listando cada caso. Diferente dos tipos polimórficos, os casos não carregam um discriminador $type.

Na prática, porém, o exemplo gerado pela interface de documentação pode enganar. No teste do autor do artigo original, o valor de exemplo exibia apenas as propriedades de CardPaymentDto, que é o primeiro caso da union.

{
  "last4": "string",
  "amount": 0.1
}

Ou seja, quem consome a API pela interface pode concluir que o endpoint sempre devolve um cartão. Por isso, vale documentar os dois formatos com exemplos explícitos ou revisar o schema anyOf gerado.

Quando dois casos serializam para o mesmo formato JSON, a documentação recomenda usar um classificador [JsonUnion] para diferenciá-los. Nesse caso, o cliente consegue identificar qual tipo recebeu.

Minimal APIs: o caminho que os controllers no .NET 11 seguem

Os recursos novos são bem-vindos, mas mostram um padrão. Os controllers no .NET 11 estão alcançando as Minimal APIs, e não o contrário.

O short circuit, por exemplo, existe nas Minimal APIs desde o .NET 8, ou seja, há três anos. Enquanto isso, a Microsoft já recomenda Minimal APIs como ponto de partida para novas Web APIs.

Outro argumento a favor das Minimal APIs é a quantidade de serviços registrados. Elas dispensam boa parte da infraestrutura do MVC, o que deixa a aplicação mais enxuta.

Tabela comparando recursos do .NET 11 entre controllers e Minimal APIs: short circuit, union types, validação assíncrona e IAuthorizationRequirementData
Os controllers alcançam as Minimal APIs em quase tudo, menos na validação assíncrona.

Union types nas Minimal APIs

Nas Minimal APIs, a mesma union funciona sem nenhuma diferença. O handler retorna PaymentDto, e o ASP.NET Core serializa o caso que estiver dentro dela.

// PaymentsEndpoints.cs
public static class PaymentsEndpoints
{
    public static IEndpointRouteBuilder MapPaymentsEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/v2/payments");
        group.MapGet("{id:int}", GetPayment);

        return app;
    }

    public static PaymentDto GetPayment(int id)
    {
        // Mesmo comportamento do controller, com menos infraestrutura
        PaymentDto payment = id == 1
            ? new CardPaymentDto("4242", 49.99m)
            : new BankTransferDto("RTC-1001", 120.00m);

        return payment;
    }
}

Com o id igual a 1, o endpoint devolve o pagamento com cartão. Com qualquer outro valor, devolve a transferência bancária.

O atributo [ShortCircuit] também funciona nas Minimal APIs, aplicado diretamente no handler. Assim, o método ShortCircuit() e o atributo convivem sem conflito.

Validação assíncrona: o recurso que os controllers no .NET 11 não têm

Quem valida requisições com Data Annotations conhece a principal limitação desse modelo: nenhuma regra podia fazer chamadas assíncronas. Para verificar se um e-mail já existe no banco, era preciso recorrer a bibliotecas como o FluentValidation.

O .NET 11 resolve isso na base. As bibliotecas ganharam a classe AsyncValidationAttribute e a interface IAsyncValidatableObject, e o Microsoft.Extensions.Validation executa essas regras quando um endpoint de Minimal API valida a requisição.

Se você ainda não usa a validação nativa, o artigo sobre validação em Minimal APIs no .NET 10 explica a base sobre a qual esse recurso foi construído.

Criando um AsyncValidationAttribute

O caminho mais simples é herdar de AsyncValidationAttribute e sobrescrever o método IsValidAsync. Dentro dele, a regra pode consultar o banco ou chamar uma API externa sem bloquear threads.

// UniqueEmailAttribute.cs
public class UniqueEmailAttribute : AsyncValidationAttribute
{
    protected override async Task<ValidationResult?> IsValidAsync(
        object? value,
        ValidationContext context,
        CancellationToken cancellationToken)
    {
        // Resolve o serviço pelo container de injeção de dependência
        var userService = context.GetRequiredService<IUserService>();

        if (value is string email
            && await userService.EmailExistsAsync(email, cancellationToken))
        {
            return new ValidationResult("That email is already registered.");
        }

        return ValidationResult.Success;
    }

    // O método síncrono também é abstrato; lançar exceção evita que a regra seja ignorada
    protected override ValidationResult? IsValid(object? value, ValidationContext context) =>
        throw new InvalidOperationException("Validate this attribute with IsValidAsync.");
}

O método síncrono IsValid também precisa ser sobrescrito. A própria documentação sugere lançar uma exceção nele, para que a regra nunca seja pulada em silêncio por uma API síncrona.

Depois disso, o atributo entra na propriedade como qualquer outro.

// RegisterEmailRequest.cs
public class RegisterEmailRequest
{
    [Required, EmailAddress, UniqueEmail]
    public string Email { get; set; } = string.Empty;
}

Por fim, a validação é ativada com builder.Services.AddValidation() no Program.cs, e o endpoint recebe a classe como parâmetro.

// UsersEndpoints.cs
public static class UsersEndpoints
{
    public static IEndpointRouteBuilder MapUsersEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/v2/users");
        group.MapPost("/register", Register);

        return app;
    }

    public static Created Register(RegisterEmailRequest request) =>
        TypedResults.Created($"/users/{request.Email}");
}

Ao enviar um e-mail já cadastrado, o endpoint responde 400 com a mensagem da regra. Portanto, a requisição passou pelo atributo assíncrono antes de chegar ao handler.

Dois cuidados merecem atenção. Primeiro, o framework pode executar o mesmo atributo em paralelo, então a implementação precisa ser thread-safe.

Além disso, o IsValidAsync deve respeitar o CancellationToken. O framework aguarda todas as validações iniciadas, então uma regra que ignora o cancelamento atrasa a resposta de erro.

O que acontece nos controllers no .NET 11

Nos controllers no .NET 11, a mesma classe de requisição não funciona como esperado. O MVC continua usando o pipeline de validação síncrono.

// UsersController.cs
[ApiController]
[Route("api/v1/users")]
public class UsersController : ControllerBase
{
    [HttpPost("register")]
    public Created Register(RegisterEmailRequest request) =>
        TypedResults.Created($"/users/{request.Email}");
}

No teste do artigo original, o controller chamou o método IsValid síncrono do UniqueEmailAttribute. Como esse método lança exceção, a requisição falhou em vez de validar o e-mail.

Assim, quem precisa de regras assíncronas em controllers continua dependendo de bibliotecas externas ou de verificações dentro da action. Esse é, hoje, o argumento técnico mais forte para escolher Minimal APIs em projetos novos.

Novidades do EF Core 11

O EF Core 11 acompanha o lançamento e traz duas mudanças que afetam o dia a dia de quem escreve APIs. Ambas valem para os controllers no .NET 11 e também para as Minimal APIs.

Criar e aplicar migration em um passo

Antes, adicionar uma migration exigia dois comandos: um para gerar e outro para atualizar o banco. Agora, a opção --add do database update faz as duas coisas.

# Package Manager Console do Visual Studio
Update-Database -Migration AddProducts -Add

# .NET CLI
dotnet ef database update AddProducts --add

O comando gera a migration, compila o código em tempo de execução com o Roslyn e aplica o resultado no banco. Ainda assim, os arquivos continuam salvos em disco para o controle de versão.

As opções --output-dir e --namespace também funcionam junto com --add. Mesmo assim, em projetos com revisão de código, gerar e revisar a migration antes de aplicar continua sendo a prática mais segura.

MinBy e MaxBy no EF Core

O EF Core 11 passou a traduzir MinBy e MaxBy para SQL. Diferente de Min e Max, esses métodos devolvem o item inteiro com o menor ou o maior valor, e não apenas o valor.

// Produto com o menor preço
var cheapest = await context.Products
    .Select(p => new { p.Id, p.Name, p.Price })
    .MinByAsync(p => p.Price);

// Produto com o maior preço
var mostExpensive = await context.Products
    .Select(p => new { p.Id, p.Name, p.Price })
    .MaxByAsync(p => p.Price);

No SQL Server, a consulta vira um SELECT TOP(1) com ORDER BY na chave escolhida. Dessa forma, o banco faz o trabalho, e a aplicação não precisa carregar a tabela inteira para descobrir o item.

Como migrar seus controllers no .NET 11

Em teoria, levar uma aplicação com controllers para o .NET 11 é simples:

  1. instale o SDK do .NET 11;
  2. troque o TargetFramework para net11.0 no .csproj;
  3. atualize os pacotes do NuGet;
  4. atualize a imagem do .NET no Dockerfile.

Se a aplicação roda em um servidor como o IIS, também é preciso instalar o runtime do .NET 11 nele. Com isso, o projeto passa a compilar com o C# 15 por padrão.

Na prática, a atualização dos pacotes costuma ser a parte mais trabalhosa ao levar os controllers no .NET 11 para produção. Algumas APIs deixam de existir, e o projeto para de compilar até que o código seja ajustado.

Além disso, o .NET 11 traz mudanças de comportamento que afetam controllers. O documento OpenAPI agora usa a versão 3.2 por padrão, e o ApiExplorer emite todos os tipos de resposta declarados para cada status code.

Antes de atualizar, também vale revisar a segurança das APIs. O artigo sobre rate limiting no ASP.NET Core 10 mostra um dos middlewares que o short circuit deixa de executar.

Vale a pena atualizar agora?

Lançado como versão STS, o .NET 11 tem dois anos de suporte, até novembro de 2028. Já o .NET 10, por ser LTS, também tem suporte encerrado em novembro de 2028.

Ou seja, quem migra do .NET 10 para o .NET 11 não ganha tempo de suporte. Pelo contrário, assume uma atualização extra e terá de migrar de novo na mesma janela.

Por isso, a recomendação do artigo original é pragmática: manter aplicações existentes no .NET 10 e usar o .NET 11 em projetos novos. A exceção são aplicações no .NET 8 ou no .NET 9, cujo suporte termina em novembro de 2026.

Para essas aplicações, a migração deixa de ser opcional. Nesse caso, a escolha entre .NET 10 e .NET 11 depende de quanto o time valoriza os recursos novos frente ao esforço de atualizar.

Conclusão

Os controllers no .NET 11 ganharam recursos úteis de verdade. O [ShortCircuit] tira health checks e endpoints simples do pipeline, e os union types permitem retornar formatos diferentes com segurança de tipos.

Além disso, autorização baseada em IAuthorizationRequirementData e múltiplos [ProducesResponseType] por status code agora funcionam no MVC. Mesmo assim, a validação assíncrona continua exclusiva das Minimal APIs.

Em resumo, os controllers no .NET 11 seguem viáveis para aplicações existentes, mas a direção da plataforma está clara. Para projetos novos, as Minimal APIs recebem os recursos primeiro e, às vezes, sozinhas.

A documentação oficial do ASP.NET Core no .NET 11 lista todas as mudanças. Vale consultá-la antes de planejar a atualização.

FAQ

O atributo ShortCircuit funciona com Minimal APIs?

Sim. O atributo funciona em handlers de Minimal APIs, e o método ShortCircuit() continua disponível sem mudanças.

Posso usar ShortCircuit em um endpoint com [Authorize]?

Não. Um endpoint com short circuit e metadados de autorização ou CORS falha com InvalidOperationException em tempo de execução.

Union types funcionam em parâmetros de rota ou query string?

Não. O ASP.NET Core aceita unions apenas no corpo JSON da requisição e da resposta.

A validação assíncrona funciona em controllers no .NET 11?

Não. O MVC continua usando a validação síncrona, então regras com AsyncValidationAttribute só funcionam nas Minimal APIs.

Vale migrar do .NET 10 para o .NET 11?

Para aplicações existentes, geralmente não, porque as duas versões perdem suporte em novembro de 2028. Para projetos novos, o .NET 11 é uma boa escolha.