O que vem por aí no ASP.NET 7

O ASP.NET 7 já tem data prevista e os primeiros previews do .NET 7 estão disponíveis para quem quer testar o que vem por aí. Neste artigo, vamos ver as primeiras mudanças que chegam com o Preview 2 da nova versão.

A Microsoft publicou esse preview em 15 de março de 2022, com novidades para Web APIs, SignalR, Minimal APIs e cookies. Por isso, vale entender cada recurso agora, antes da versão final.

Instalando o .NET 7 para testar o ASP.NET 7

Até a escrita deste artigo, o .NET 7 está em sua segunda versão de preview. Para conferir todas as novidades do ASP.NET 7, você precisa instalar o SDK desta versão.

Não se preocupe, porque as versões anteriores do .NET na sua máquina continuam intactas. O .NET roda lado a lado, então não é necessário remover instalações antigas.

Depois da instalação, confira as versões disponíveis com o comando abaixo:

dotnet --list-sdks

Na prática, cada projeto usa o SDK mais recente instalado, a menos que exista um arquivo global.json fixando outra versão. Dessa forma, você consegue testar o preview sem afetar projetos que precisam continuar no .NET 6.

Atualizando seu projeto

Caso tenha criado algum projeto em .NET 6 e queira atualizar para o 7, basta alterar as dependências no arquivo .csproj, apontando para a versão 7.

  • Microsoft.AspNetCore.* ⇒ 7.0.0-preview.2.*
  • Microsoft.Extensions.* ⇒ 7.0.0-preview.2.*

Além disso, troque o TargetFramework do projeto para net7.0. Um arquivo de projeto mínimo fica assim:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <!-- aponta o projeto para o .NET 7 -->
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Se o projeto usa Blazor WebAssembly, o anúncio oficial também indica a instalação das ferramentas de build com dotnet workload install wasm-tools.

Inferência de parâmetros nos controllers do ASP.NET 7

Se você conferiu nosso artigo sobre Minimal APIs, provavelmente notou que não usamos [FromServices], [FromBody] ou outros atributos [From*] nos parâmetros dos métodos que respondem às rotas.

Isso acontece porque o ASP.NET infere esses parâmetros, buscando nas dependências, no corpo da requisição, no cabeçalho e até na query string.

Esse recurso também chega aos controllers no ASP.NET 7. Assim, não é mais necessário especificar de onde o parâmetro vem, pelo menos na maioria dos casos.

builder.Services.AddScoped<SomeCustomType>();

[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
    // Ambos os métodos vão funcionar
    public ActionResult GetWithAttribute([FromServices] SomeCustomType service) => Ok();

    [Route("noAttribute")]
    public ActionResult Get(SomeCustomType service) => Ok();
}

No exemplo, o tipo SomeCustomType está registrado no container de injeção de dependência. Por isso, o framework entende que o parâmetro deve vir dos serviços, mesmo sem o atributo.

Como a inferência funciona no ASP.NET 7

Segundo a documentação What's new in ASP.NET Core 7.0, o framework verifica os tipos do container na inicialização da aplicação. Para isso, ele usa a interface IServiceProviderIsService.

Em outras palavras, se o tipo do parâmetro existe como serviço, ele vem da injeção de dependência. Caso contrário, o ASP.NET segue as regras de binding tradicionais, como corpo, rota e query string.

A documentação lista as regras que o novo mecanismo aplica a cada parâmetro de uma action, nesta ordem:

  1. Uma origem de binding definida explicitamente nunca muda.
  2. Um tipo complexo registrado no container vem dos serviços.
  3. Já um tipo complexo fora do container vem do corpo da requisição.
  4. Um parâmetro com o mesmo nome de um valor de rota vem da rota.
  5. Todos os demais parâmetros vêm da query string.

Portanto, um parâmetro marcado com [FromBody] continua vindo do corpo, mesmo que o tipo também esteja registrado no container. Da mesma forma, tipos simples como int e string seguem as regras de rota e query string.

Existe ainda um detalhe sobre o container. A interface IServiceProviderIsService é opcional, então containers de terceiros que não a implementam não oferecem essa inferência.

Se você quer revisar os fundamentos desse mecanismo, o artigo sobre Dependency Injection no ASP.NET Core explica o container nativo em detalhes.

Desabilitando a inferência no ASP.NET 7

A Microsoft classificou essa mudança como breaking change, embora a chance de impacto seja baixa. O problema só aparece quando um tipo registrado no container também chega como argumento de uma action vindo de outra origem.

Caso queira desabilitar esse comportamento, basta definir a propriedade DisableImplicitFromServicesParameters como true nas configurações da sua API, como no código abaixo.

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    // volta ao comportamento anterior, sem inferência a partir dos serviços
    options.DisableImplicitFromServicesParameters = true;
});

Com a opção desligada, você ainda pode usar [FromServices] nos parâmetros que precisam vir da injeção de dependência. Assim, a decisão fica explícita, parâmetro a parâmetro.

SignalR agora suporta DI nos métodos do hub

O SignalR também recebe uma atualização no ASP.NET 7, que permite a injeção de dependências nos métodos dos hubs. Assim como nos controllers, não precisamos mais do [FromServices].

builder.Services.AddScoped<SomeCustomType>();

public class MyHub : Hub
{
    // SomeCustomType agora vem da DI por padrão
    public Task Method(string text, SomeCustomType type) => Task.CompletedTask;
}

Antes dessa mudança, um tipo aceito no método do hub sempre vinha da mensagem enviada pelo cliente. Agora, se o tipo existe no container, o SignalR o resolve pela injeção de dependência, e o cliente recebe um erro se tentar enviá-lo.

A documentação explica o motivo da mudança. Com ela, cada método recebe apenas os serviços que usa, sem a necessidade de injetar todas as dependências no construtor do hub.

Por exemplo, um DbContext usado só em uma operação rápida de escrita pode entrar apenas no método que precisa dele. Para conhecer melhor o SignalR, confira o artigo Minimal APIs com SignalR.

public class ChatHub : Hub
{
    // o DbContext entra só no método que grava a mensagem
    public async Task SendMessage(string user, string message, AppDbContext context)
    {
        context.Messages.Add(new Message { User = user, Content = message });
        await context.SaveChangesAsync();

        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

A injeção pelo construtor continua disponível e segue útil para serviços usados por vários métodos. No entanto, para dependências pontuais, a injeção no método deixa o hub mais enxuto e fácil de testar.

Configurando a DI do SignalR

Caso queira desabilitar esse comportamento, defina a propriedade DisableImplicitFromServicesParameters como true nas opções do SignalR, como no exemplo abaixo.

builder.Services.AddSignalR(options =>
{
    // os parâmetros voltam a vir do cliente por padrão
    options.DisableImplicitFromServicesParameters = true;
});

Caso queira deixar explícita a origem do parâmetro, você ainda pode usar o [FromServices], sem problema algum.

public class MyHub : Hub
{
    // a origem do parâmetro fica explícita
    public Task Method(string arguments, [FromServices] SomeCustomType type)
        => Task.CompletedTask;
}

Essa combinação é útil quando você quer adotar o recurso sem quebrar clientes antigos. Nesse caso, desligue a inferência e marque com [FromServices] apenas os parâmetros novos.

Descrição e sumário nas Minimal APIs do ASP.NET 7

Outra adição do ASP.NET 7 às Minimal APIs é a possibilidade de informar uma descrição e um sumário para cada endpoint. Depois, essas informações aparecem na documentação da API gerada com OpenAPI.

app.MapGet("/hello", () => "Hello world!")
    .WithDescription("Sends a request to the backend HelloService to process a greeting request.");

// Ou, usando atributo no próprio delegate
app.MapGet("/hello", [EndpointSummary("Sends a Hello request to the backend")] () => "Hello world!");

Repare que o atributo fica no delegate, e não no método MapGet. Além disso, a versão final do .NET 7 também trouxe o método WithSummary e o atributo [EndpointDescription], completando as duas formas de configuração.

O sumário costuma ser uma frase curta, exibida ao lado da rota na interface do Swagger. Por outro lado, a descrição traz detalhes mais longos sobre o comportamento do endpoint.

Se você quer aprofundar o tema, o artigo sobre documentação de APIs com OpenAPI, Swagger e Scalar mostra como esses metadados chegam à interface.

Arrays e StringValues no ASP.NET 7

Outra adição às Minimal APIs é o binding de valores do cabeçalho HTTP e da query string para arrays de tipos primitivos, arrays de strings ou StringValues.

// Bind de valores da query string para um array de tipo primitivo
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) => $"tag1: {q[0]}, tag2: {q[1]}, tag3: {q[2]}");

// Bind para um array de strings
// GET /tags?names=john&names=jack&names=jane
app.MapGet("/tags", (string[] names) => $"tag1: {names[0]}, tag2: {names[1]}, tag3: {names[2]}");

// Bind para StringValues
// GET /tags?names=john&names=jack&names=jane
app.MapGet("/tags", (StringValues names) => $"tag1: {names[0]}, tag2: {names[1]}, tag3: {names[2]}");

O tipo StringValues, do namespace Microsoft.Extensions.Primitives, representa zero, um ou vários valores de string. Por isso, ele combina bem com query strings e cabeçalhos, que aceitam a mesma chave repetida.

A documentação traz ainda um detalhe importante. Quando nenhum valor chega na query string, um parâmetro string[] recebe um array vazio, e não null.

Para ler valores repetidos de um cabeçalho, use o atributo [FromHeader] com o nome da chave. Assim, o endpoint recebe todos os valores enviados naquele cabeçalho.

// GET /orders/header-ids
// Envie vários cabeçalhos X-Order-Id com valores diferentes
app.MapGet("/orders/header-ids", ([FromHeader(Name = "X-Order-Id")] int[] ids) => Results.Ok(ids));

Testando o binding de arrays

Para ver o binding funcionando, execute a aplicação com dotnet run e faça chamadas pelo terminal. O curl permite repetir a mesma chave na query string e o mesmo cabeçalho várias vezes.

# envia três valores para a mesma chave da query string
curl "https://localhost:5001/tags?q=1&q=2&q=3"

# envia dois cabeçalhos com a mesma chave
curl -H "X-Order-Id: 10" -H "X-Order-Id: 20" https://localhost:5001/orders/header-ids

Na primeira chamada, o parâmetro q recebe o array com os três números. Na segunda, o parâmetro ids recebe os valores 10 e 20, já convertidos para inteiros.

Ajuste a porta de acordo com o perfil de execução do seu projeto. Em geral, ela aparece no terminal assim que a aplicação sobe.

Binding de tipos complexos com TryParse

No caso das query strings, também podemos fazer binding de valores para arrays de tipos complexos. Para isso, o tipo precisa implementar um método estático TryParse.

// Bind para um array de tipo complexo
// GET /tags?tags=trendy&tags=hot&tags=spicy
app.MapGet("/tags", (Tag[] tags) =>
{
    return Results.Ok(tags);
});

class Tag
{
    public string? TagName { get; init; }

    // o framework chama este método para cada valor recebido
    public static bool TryParse(string? tagName, out Tag tag)
    {
        if (tagName is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { TagName = tagName };
        return true;
    }
}

Quando o TryParse retorna false, o framework responde com status 400. Dessa forma, valores inválidos não chegam ao seu código.

Customizar o consentimento dos cookies

O ASP.NET 7 adiciona a propriedade CookiePolicyOptions.ConsentCookieValue para acompanhar e customizar o consentimento do usuário em relação aos cookies que armazenamos.

Nas versões anteriores, a validação de consentimento usava sempre o valor yes no cookie. Agora, você escolhe o valor que representa o consentimento, como true.

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;

    // valor gravado no cookie quando o usuário aceita
    options.ConsentCookieValue = "true";
});

Esse ajuste ajuda a integrar o ASP.NET 7 com ferramentas de consentimento que já usam outro padrão de valor. Além disso, ele facilita o atendimento a regras de privacidade como a GDPR e a LGPD.

Shadow copying no IIS

O anúncio do Preview 2 também trouxe um recurso experimental para quem hospeda aplicações no IIS. No Windows, o .NET bloqueia os binários da aplicação em execução, o que impede a troca dos arquivos durante um deploy.

Com o shadow copying, o ASP.NET Core Module copia os assemblies para outra pasta antes de carregá-los. Assim, você consegue substituir os arquivos originais sem parar a aplicação.

<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout">
  <handlerSettings>
    <!-- habilita a cópia dos assemblies antes do carregamento -->
    <handlerSetting name="experimentalEnableShadowCopy" value="true" />
    <handlerSetting name="shadowCopyDirectory" value="../ShadowCopyDirectory/" />
  </handlerSettings>
</aspNetCore>

Mesmo assim, a documentação deixa claro que o recurso não elimina o tempo de indisponibilidade. Ou seja, o IIS ainda recicla a aplicação, e algumas requisições podem receber 503.

Exemplo completo com ASP.NET 7

Para juntar as peças, veja um Program.cs que combina controllers, SignalR e Minimal APIs no mesmo projeto. O código registra um único serviço e o consome nos três estilos.

var builder = WebApplication.CreateBuilder(args);

// serviço disponível para controllers, hubs e endpoints
builder.Services.AddScoped<GreetingService>();
builder.Services.AddControllers();
builder.Services.AddSignalR();

var app = builder.Build();

app.MapControllers();
app.MapHub<GreetingHub>("/hubs/greeting");

// Minimal API com sumário para a documentação
app.MapGet("/greetings", (string[] names, GreetingService service) =>
        names.Select(service.Greet))
    .WithSummary("Greets every name received in the query string");

app.Run();

public class GreetingService
{
    public string Greet(string name) => $"Hello, {name}!";
}

[Route("[controller]")]
[ApiController]
public class GreetingController : ControllerBase
{
    // o serviço vem da DI, sem [FromServices]
    [HttpGet("{name}")]
    public ActionResult Get(string name, GreetingService service)
        => Ok(service.Greet(name));
}

public class GreetingHub : Hub
{
    // o serviço também vem da DI no método do hub
    public Task Greet(string name, GreetingService service)
        => Clients.All.SendAsync("Greeted", service.Greet(name));
}

No controller, o parâmetro name vem da rota, porque aparece no template {name}. Já o GreetingService vem do container, sem nenhum atributo.

No hub, o cliente envia apenas o name, enquanto o SignalR resolve o serviço sozinho. Por fim, o endpoint /greetings recebe os nomes como array a partir da query string.

Esse exemplo mostra o principal ganho do ASP.NET 7 nessa área. Os três modelos de programação passam a seguir a mesma lógica de inferência, o que reduz atributos e deixa o código mais previsível.

Quando usar o ASP.NET 7

Previews servem para testar e dar feedback, e não para rodar em produção. Por isso, use o ASP.NET 7 Preview 2 em projetos de estudo ou em branches separadas.

O .NET 7 chegou à versão final em novembro de 2022 como uma versão STS, de suporte padrão. Portanto, seu suporte oficial terminou em 14 de maio de 2024, segundo a página de ciclo de vida da Microsoft.

Antes de atualizar um projeto real, leia a lista de breaking changes do .NET 7 na documentação oficial. As duas mudanças de injeção de dependência deste artigo aparecem nessa lista, junto com a forma de desligar cada uma.

Em seguida, rode a suíte de testes da aplicação com o novo SDK. Dessa forma, você descobre cedo se algum tipo registrado no container conflita com parâmetros que antes vinham do corpo ou do cliente.

Mesmo assim, entender essas novidades continua útil, porque os recursos seguiram nas versões seguintes. A injeção nos hubs, o binding de arrays e os metadados de OpenAPI fazem parte do dia a dia de quem desenvolve APIs com ASP.NET Core.

Conclusão

O Preview 2 mostra a direção do ASP.NET 7: menos atributos repetitivos e mais inferência inteligente. Controllers e hubs do SignalR passam a resolver serviços sozinhos, como as Minimal APIs já faziam.

Além disso, as Minimal APIs ganham binding de arrays, StringValues e tipos complexos, junto com descrições e sumários para a documentação. Por fim, o consentimento de cookies e o shadow copying no IIS completam o pacote.

Instale o SDK, crie um projeto de teste e experimente cada recurso do ASP.NET 7. Assim, você chega à versão final já sabendo o que muda no seu código.

Se encontrar algum comportamento inesperado, registre o problema no repositório do ASP.NET Core no GitHub, porque esse é justamente o objetivo de cada preview.

FAQ

Instalar o .NET 7 Preview remove o .NET 6 da minha máquina?

Não. As versões do .NET rodam lado a lado, e você pode fixar a versão de cada projeto com um arquivo global.json.

Ainda posso usar o atributo FromServices no ASP.NET 7?

Sim. O atributo continua funcionando e deixa explícita a origem do parâmetro em controllers e hubs.

Como desligo a injeção automática nos controllers?

Configure ApiBehaviorOptions e defina DisableImplicitFromServicesParameters como true.

A injeção nos métodos do hub quebra clientes do SignalR?

Pode quebrar se o cliente enviava um tipo que também está registrado no container. Nesse caso, desligue a inferência nas opções do SignalR e use [FromServices] só nos parâmetros novos.

Posso usar um preview do .NET 7 em produção?

Não é recomendado. Previews servem para testes e feedback, e o próprio .NET 7 já está fora do suporte oficial desde maio de 2024.