Compartilhar via


Novidades do ASP.NET Core 9.0

Este artigo destaca as alterações mais significativas no ASP.NET Core 9.0 com links para a documentação relevante.

Este artigo foi atualizado para o .NET 9 versão prévia 7.

Blazor

Esta seção descreve os novos recursos do Blazor.

.NET MAUIBlazor Hybrid e um modelo de solução de Aplicativo Web

Um novo modelo de solução facilita a criação de aplicativos de cliente Web do Blazor e nativos do .NET MAUI que compartilham a mesma interface do usuário. Esse modelo mostra como criar aplicativos cliente que maximizam a reutilização de código e opções para Android, iOS, Mac, Windows e Web.

Os principais recursos deste modelo incluem:

  • A capacidade de escolher um modo de renderização interativo Blazor para o aplicativo Web.
  • Criação automática dos projetos apropriados, incluindo um aplicativo Blazor para a web (renderização automática interativa global) e um aplicativo .NET MAUIBlazor Hybrid.
  • Os projetos criados usam uma biblioteca de classes do Razor (RCL) compartilhada para manter os componentes Razor da interface do usuário.
  • Está incluída uma amostra de código que demonstra como usar a injeção de dependências para fornecer diferentes implementações de interface para o aplicativo Blazor Hybrid e o Aplicativo Web do Blazor.

Para começar, instale o SDK do .NET 9 e instale a carga de trabalho do .NET MAUI que contém o modelo:

dotnet workload install maui

Crie uma solução a partir do modelo de projeto em um shell de comando usando o seguinte comando:

dotnet new maui-blazor-web

O modelo também está disponível no Visual Studio.

Observação

Atualmente, ocorrerá uma exceção se os modos de renderização do Blazor forem definidos no nível por página/componente. Para obter mais informações, consulte O BlazorWebView precisa de uma maneira de habilitar a substituição de ResolveComponentForRenderMode (dotnet/aspnetcore nº 51235).

Para obter mais informações, confira Criar um aplicativo .NET MAUIBlazor Hybrid com um aplicativo web do Blazor.

Otimização de entrega de ativos estáticos

MapStaticAssets é um novo middleware que ajuda a otimizar a entrega de ativos estáticos em qualquer aplicativo do ASP.NET Core, incluindo aplicativos Blazor.

Para obter mais informações, consulte um dos seguintes recursos:

Detectar o local de renderização, a interatividade e o modo de renderização atribuído no runtime

Introduzimos uma nova API projetada para simplificar o processo de consulta de estados de componentes no runtime. Essa API fornece os seguintes recursos:

  • Determinar o local de execução atual do componente, o que pode ser particularmente útil para depurar e otimizar o desempenho do componente.
  • Verificar se o componente está em execução em um ambiente interativo, o pode ser útil para componentes que têm comportamentos diferentes com base na interatividade de seu ambiente.
  • Recuperar o modo de renderização atribuído ao componente: entender o modo de renderização pode ajudar a otimizar o processo de renderização e aprimorar o desempenho de um componente de modo geral.

Para obter mais informações, consulte ASP.NET Core Blazor modos de renderização.

Experiência de reconexão aprimorada do lado do servidor:

Os seguintes aprimoramentos foram feitos na experiência de reconexão padrão do lado do servidor:

  • Quando o usuário navega de volta para um aplicativo com um circuito desconectado, a reconexão é tentada imediatamente, em vez de aguardar todo o intervalo de tempo até a próxima reconexão. Isso aprimora a experiência do usuário quando este navega até um aplicativo em uma guia do navegador que entrou no modo de suspensão.

  • Quando uma tentativa de reconexão chega ao servidor, mas o servidor já liberou o circuito, a página é atualizada automaticamente. Isso impede que o usuário precise atualizar a página manualmente se o provável resultado for uma reconexão bem-sucedida.

  • O tempo decorrido até a reconexão usa uma estratégia de espera calculada. Por padrão, as primeiras várias tentativas de reconexão ocorrem em rápida sucessão sem um intervalo de repetição, antes que os atrasos calculados sejam introduzidos entre as tentativas. Você pode personalizar o comportamento do intervalo de repetição especificando uma função para calcular o intervalo de repetição, como demonstra o exemplo de espera exponencial a seguir:

    Blazor.start({
      circuit: {
        reconnectionOptions: {
          retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
            previousAttempts >= maxRetries ? null : previousAttempts * 1000
        },
      },
    });
    
  • O estilo da interface do usuário de reconexão padrão foi modernizado.

Para obter mais informações, confira as diretrizes BlazorSignalRdo ASP.NET Core.

Serialização de estado de autenticação simplificada para Aplicativos Web Blazor

Novas APIs facilitam o acréscimo de autenticação a um Aplicativo Web do Blazor existente. Quando você cria um novo Aplicativo Web do Blazor com autenticação usando Contas Individuais e habilita a interatividade baseada em WebAssembly, o projeto inclui um AuthenticationStateProvider personalizado tanto nos projetos do servidor quanto do cliente.

Esses provedores fazem com que o estado de autenticação do usuário flua para o navegador. A autenticação no servidor em vez de no cliente permite que o aplicativo acesse o estado de autenticação durante a pré-renderização e antes que o runtime do .NET WebAssembly seja inicializado.

As implementações de AuthenticationStateProvider personalizadas usam o Serviço do Estado de Componente Persistente (PersistentComponentState) para serializar o estado de autenticação em comentários HTML e, em seguida, lê-lo de volta a partir do WebAssembly para criar uma nova instância de AuthenticationState.

Isso funcionará bem se você tiver começado a partir do modelo de projeto do Aplicativo Web do Blazor e selecionado a opção Contas Individuais, mas envolve muito código para implementar por conta própria ou para copiar, se você estiver tentando adicionar autenticação a um projeto existente. Agora temos APIs, que já fazem parte do modelo de projeto do Aplicativo Web do Blazor e podem ser chamadas nos projetos do servidor e do cliente para adicionar essa funcionalidade:

  • AddAuthenticationStateSerialization: adiciona os serviços necessários para serializar o estado de autenticação no servidor.
  • AddAuthenticationStateDeserialization: adiciona os serviços necessários para desserializar o estado de autenticação no navegador.

Por padrão, a API só serializa as declarações de nome e função do lado do servidor para acesso no navegador. Uma opção pode ser repassada para AddAuthenticationStateSerialization de modo a incluir todas as declarações.

Para obter mais informações, consulte as seguintes seções de aplicativos Blazor seguros do lado do servidor do ASP.NET Core:

Adicionar páginas de SSR (renderização do lado do servidor) estático a um aplicativo Web Blazor globalmente interativo

Com o lançamento do .NET 9, agora é mais simples adicionar páginas SSR estáticas a aplicativos que adotam interatividade global.

Essa abordagem só é útil quando o aplicativo tem páginas específicas que não podem funcionar com a renderização interativa do Servidor ou WebAssembly. Por exemplo, adote essa abordagem para páginas que dependem da leitura/gravação de HTTP e só podem funcionar em um ciclo de solicitação/resposta em vez de renderização interativa. Para páginas que funcionam com renderização interativa, você não deve forçá-las a usar a renderização SSR estática, pois ela é menos eficiente e menos responsiva para o usuário final.

Marque qualquer página de componente Razor com o novo atributo [ExcludeFromInteractiveRouting] atribuído com a diretiva @attributeRazor:

@attribute [ExcludeFromInteractiveRouting]

A aplicação do atributo faz com que a navegação na página saia do roteamento interativo. A navegação de entrada é forçada a executar um recarregamento de página inteira, resolvendo a página por meio de roteamento interativo. O recarregamento de página inteira força o componente raiz de nível superior, normalmente o componente App (App.razor), a gerar novamente do servidor, permitindo que o aplicativo mude para um modo de renderização de nível superior diferente.

O método de extensão HttpContext.AcceptsInteractiveRouting permite que o componente detecte se [ExcludeFromInteractiveRouting] é aplicado à página atual.

No componente App, use o padrão no exemplo a seguir:

  • Páginas que não são anotadas com [ExcludeFromInteractiveRouting] padrão para o modo de renderização InteractiveServer com interatividade global. Você pode substituir InteractiveServer por InteractiveWebAssembly ou InteractiveAuto para especificar um modo de renderização global padrão diferente.
  • Páginas anotadas com [ExcludeFromInteractiveRouting] adotar SSR estático (PageRenderMode é atribuído null).
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}

Uma alternativa ao uso do método de extensão HttpContext.AcceptsInteractiveRouting é ler metadados de ponto de extremidade manualmente usando HttpContext.GetEndpoint()?.Metadata.

Esse recurso é abordado pela documentação de referência nos Modos de renderização Blazor do ASP.NET Core .

Injeção de construção

Os componentes Razor dão suporte à injeção do construtor.

No exemplo a seguir, a classe parcial (code-behind) injeta o serviço NavigationManager usando um construtor primário:

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

Para saber mais, confira Injeção de dependência do Blazor no ASP.NET Core.

Compactação de Websocket para componentes do Servidor Interativo

Por padrão, os componentes do Interactive Server habilitam a compactação para conexões WebSocket e definem uma diretiva frame-ancestors Content Security Policy (CSP) definida como 'self', que permite apenas incorporar o aplicativo em um <iframe> da origem a partir da qual o aplicativo é servido quando a compactação é habilitado ou quando uma configuração para o contexto WebSocket é fornecida.

A compactação pode ser desabilitada definindo ConfigureWebSocketOptions como null, o que reduz a vulnerabilidade do aplicativo a ataques, mas pode resultar em desempenho reduzido:

.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

Configure um CSP frame-ancestors mais restrito com um valor de 'none' (aspas simples obrigatórias), que permite a compactação do WebSocket mas impede que os navegadores incorporem o aplicativo em qualquer <iframe>:

.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

Para saber mais, consulte os recursos a seguir:

Manipular eventos de composição de teclado no Blazor

A nova propriedade KeyboardEventArgs.IsComposing indica se o evento de teclado faz parte de uma sessão de composição. Acompanhar o estado de composição de eventos de teclado é crucial para lidar com métodos de entrada de caracteres internacionais.

Parâmetro OverscanCount adicionado a QuickGrid

O componente QuickGrid agora expõe uma propriedade OverscanCount que especifica quantas linhas adicionais são renderizadas antes e depois da região visível quando a virtualização está habilitada.

O OverscanCount padrão é 3. O exemplo a seguir aumenta o OverscanCount para 4:

<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
    ...
</QuickGrid>

O componente InputNumber é compatível com o atributo type="range"

O componente InputNumber<TValue> agora é compatível com o atributo type="range", que cria uma entrada de intervalo compatível com o model binding e a validação de formulário, normalmente renderizada como um controle deslizante ou de discagem em vez de uma caixa de texto:

<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">
    <div>
        <label>
            Nacelle Count (2-6): 
            <InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2" 
                step="1" type="range" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private EngineSpecifications? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() {}

    public class EngineSpecifications
    {
        [Required, Range(minimum: 2, maximum: 6)]
        public int NacelleCount { get; set; }
    }
}

SignalR

Esta seção descreve os novos recursos do SignalR.

Suporte a tipos polimórficos em Hubs SignalR

Os métodos de hub agora podem aceitar uma classe base em vez da classe derivada para habilitar cenários polimórficos. O tipo base precisa ser anotado para permitir o polimorfismo.

public class MyHub : Hub
{
    public void Method(JsonPerson person)
    {
        if (person is JsonPersonExtended)
        {
        }
        else if (person is JsonPersonExtended2)
        {
        }
        else
        {
        }
    }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
    public string Name { get; set; }
    public Person Child { get; set; }
    public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson
{
    public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson
{
    public string Location { get; set; }
}

Atividades aprimoradas para SignalR

SignalR agora tem um ActivitySource chamado Microsoft.AspNetCore.SignalR.Server que emite eventos para chamadas de método de hub:

  • Cada método é sua própria atividade, portanto, qualquer coisa que emita uma atividade durante a chamada do método hub está sob a atividade do método hub.
  • As atividades do método hub não têm um pai. Isso significa que eles não são agrupados sob a conexão de SignalR de execução longa.

O exemplo a seguir usa o painel .NET Aspire e os pacotes OpenTelemetry:

<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />

Adicione o seguinte código de inicialização ao arquivo Program.cs:

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        if (builder.Environment.IsDevelopment())
        {
            // We want to view all traces in development
            tracing.SetSampler(new AlwaysOnSampler());
        }

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
    });

builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());

Veja a seguir o exemplo de saída do Painel do Aspire:

Lista de atividades para eventos de chamada de método do Hub SignalR

SignalR oferece suporte a corte AOT nativa

Continuando a jornada do AOT nativo iniciada no .NET 8, habilitamos o suporte ao corte e à compilação AOT (antecipada) nativa para cenários de cliente e de servidor de SignalR. Agora você pode aproveitar os benefícios de desempenho do uso da AOT nativa em aplicativos que usam SignalR para comunicações Web em tempo real.

Introdução

Instale o SDK do .NET 9 mais recente.

Crie uma solução a partir do webapiaotmodelo em um shell de comando usando o seguinte comando:

dotnet new webapiaot -o SignalRChatAOTExample

Substitua o conteúdo do Program.cs arquivo pelo seguinte SignalR código:

using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
    o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
    <title>SignalR Chat</title>
</head>
<body>
    <input id="userInput" placeholder="Enter your name" />
    <input id="messageInput" placeholder="Type a message" />
    <button onclick="sendMessage()">Send</button>
    <ul id="messages"></ul>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub")
            .build();

        connection.on("ReceiveMessage", (user, message) => {
            const li = document.createElement("li");
            li.textContent = `${user}: ${message}`;
            document.getElementById("messages").appendChild(li);
        });

        async function sendMessage() {
            const user = document.getElementById("userInput").value;
            const message = document.getElementById("messageInput").value;
            await connection.invoke("SendMessage", user, message);
        }

        connection.start().catch(err => console.error(err));
    </script>
</body>
</html>
""", "text/html"));

app.Run();

[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

O exemplo anterior produz um executável nativo do Windows de 10 MB e um executável do Linux de 10,9 MB.

Limitações

  • Atualmente, apenas o protocolo JSON é suportado:
    • Conforme mostrado no código anterior, os aplicativos que usam a serialização JSON e o AOT nativo devem usar o gerador de fonte System.Text.Json.
    • Isso segue a mesma abordagem das APIs mínimas.
  • No SignalRservidor, não há suporte para parâmetros de método Hub do tipo IAsyncEnumerable<T> e ChannelReader<T> onde T é um ValueType (ou seja, struct). O uso desses tipos resulta em uma exceção de runtime na inicialização, no desenvolvimento e no aplicativo publicado. Para obter mais informações, consulte SignalR: Usando IAsyncEnumerable<T> e ChannelReader<T> com ValueTypes no AOT nativo (dotnet/aspnetcore #56179).
  • Não há suporte para Hubs fortemente tipados com AOT Nativo (PublishAot). O uso de hubs fortemente tipados com AOT nativo resultará em avisos durante a compilação e a publicação e uma exceção de runtime. Há suporte para o uso de hubs fortemente tipados com corte (PublishedTrimmed).
  • Somente Task, Task<T>, ValueTask, ou ValueTask<T> são suportados para tipos de retorno assíncronos.

APIs mínimas

Essa seção descreve novos recursos para APIs mínimas.

InternalServerError e InternalServerError<TValue> foram adicionados a TypedResults

A classe TypedResults é um veículo útil para retornar respostas baseadas em código de status HTTP fortemente tipado de uma API mínima. TypedResults agora inclui métodos e tipos de fábrica para retornar respostas de "500 Erro Interno do Servidor" de pontos de extremidade. Este é um exemplo que retorna uma resposta 500:

var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));

app.Run();

Grupos de chamadas ProducesProblem e ProducesValidationProblem de rotas

Os métodos de extensão ProducesProblem e ProducesValidationProblem foram atualizados para dar suporte ao seu uso em grupos de rotas. Esses métodos indicam que todos os pontos de extremidade em um grupo de roteiros podem retornar respostas ProblemDetails ou ValidationProblemDetails para fins de metadados de OpenAPI.

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem();

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, boolean IsCompleted);

OpenAPI

Esta seção descreve os novos recursos da OpenAPI

Suporte interno para geração de documentos OpenAPI

A especificação OpenAPI é um padrão para descrever APIs HTTP. O padrão permite que os desenvolvedores definam a forma de APIs que podem ser conectadas a geradores de cliente, geradores de servidor, ferramentas de teste, documentação e muito mais. No .NET 9 Preview, o ASP.NET Core fornece suporte interno para gerar documentos OpenAPI que representam APIs baseadas em controlador ou mínimas por meio do pacote Microsoft.AspNetCore.OpenApi.

As seguintes chamadas de código realçadas:

  • AddOpenApi para registrar as dependências necessárias no contêiner de DI do aplicativo.
  • MapOpenApi para registrar os pontos de extremidade OpenAPI necessários nas rotas do aplicativo.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();

Instale o pacote Microsoft.AspNetCore.OpenApi no projeto usando o seguinte comando:

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

Execute o aplicativo e navegue até openapi/v1.json para exibir o documento OpenAPI gerado:

Documento do OpenAPI

Documentos OpenAPI também podem ser gerados no tempo de build adicionando o pacote Microsoft.Extensions.ApiDescription.Server:

dotnet add package Microsoft.Extensions.ApiDescription.Server --prerelease

No arquivo de projeto do aplicativo, adicione o seguinte:

<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</PropertyGroup>

Execute dotnet build e inspecione o arquivo JSON gerado no diretório do projeto.

Geração de documentos OpenAPI em tempo de build

A geração de documentos OpenAPI interna do ASP.NET Core oferece suporte para várias personalizações e opções. Ela fornece transformadores de documento e operação e tem a capacidade de gerenciar vários documentos OpenAPI para o mesmo aplicativo.

Para saber mais sobre os novos recursos de documento OpenAPI do ASP.NET Core, consulte os novos documentos Microsoft.AspNetCore.OpenApi.

Aprimoramentos de preenchimento do Intellisense para o pacote OpenAPI

ASP.NET Suporte a OpenAPI do Core agora é mais acessível e amigável. As APIs OpenAPI são enviadas como um pacote independente, separado da estrutura compartilhada. Até agora, isso significava que os desenvolvedores não tinham a conveniência de recursos de preenchimento de código, como o Intellisense para APIs OpenAPI.

Reconhecendo essa lacuna, introduzimos um novo provedor de preenchimento e um corredor de código. Essas ferramentas foram projetadas para facilitar a descoberta e o uso de APIs OpenAPI, facilitando a integração do OpenAPI aos desenvolvedores em seus projetos. O provedor de preenchimento oferece sugestões de código em tempo real, enquanto o corredor de código auxilia na correção de erros comuns e na melhoria do uso da API. Esse aprimoramento faz parte do nosso compromisso contínuo de melhorar a experiência do desenvolvedor e simplificar os fluxos de trabalho relacionados à API.

Quando um usuário digita uma instrução em que uma API relacionada ao OpenAPI está disponível, o provedor de preenchimento exibe uma recomendação para a API. Por exemplo, nas capturas de tela a seguir, os preenchimentos para AddOpenApi e MapOpenApi são fornecidos quando um usuário está inserindo uma instrução de invocação em um tipo com suporte, como IEndpointConventionBuilder:

Conclusões de OpenAPI

Quando o preenchimento é aceito, e o pacote Microsoft.AspNetCore.OpenApi não é instalado, um codefixer fornece um atalho para instalar automaticamente a dependência no projeto.

Instalação automática do pacote

Suporte para atributos [Required] e [DefaultValue] em parâmetros e propriedades

Quando os atributos [Required] e [DefaultValue] são aplicados em parâmetros ou propriedades dentro de tipos complexos, a implementação do OpenAPI mapeia-os para as propriedades required e default no documento OpenAPI associado ao parâmetro ou esquema de tipo.

Por exemplo, a API a seguir produz o esquema que acompanha o tipo de Todo.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}
{
	"required": [
	  "title",
	  "description",
	  "createdOn"
	],
	"type": "object",
	"properties": {
	  "id": {
	    "type": "integer",
	    "format": "int32"
	  },
	  "title": {
	    "type": "string"
	  },
	  "description": {
	    "type": "string",
	    "default": "A new todo"
	  },
	  "createdOn": {
	    "type": "string",
	    "format": "date-time"
	  }
	}
}

Suporte para transformadores de esquema em documentos OpenAPI

O suporte interno ao OpenAPI agora é fornecido com suporte para transformadores de esquema que podem ser usados para modificar esquemas gerados por System.Text.Json e a implementação da OpenAPI. Assim como os transformadores de documento e operação, os transformadores de esquema podem ser registrados no objeto OpenApiOptions. Por exemplo, o exemplo de código a seguir demonstra o uso de um transformador de esquema para adicionar um exemplo ao esquema de um tipo.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.OpenApi.Any;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.UseSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.Type == typeof(Todo))
        {
            schema.Example = new OpenApiObject
            {
                ["id"] = new OpenApiInteger(1),
                ["title"] = new OpenApiString("A short title"),
                ["description"] = new OpenApiString("A long description"),
                ["createdOn"] = new OpenApiDateTime(DateTime.Now)
            };
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}

Melhorias nas APIs de registro do transformador em Microsoft.AspNetCore.OpenApi

Os transformadores OpenAPI dão suporte à modificação do documento OpenAPI, operações dentro do documento ou esquemas associados a tipos na API. As APIs para registrar transformadores em um documento OpenAPI fornecem uma variedade de opções para registrar transformadores.

Anteriormente, as seguintes APIs estavam disponíveis para registrar transformadores:

OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions UseTransformer<IOpenApiDocumentTransformer>()
OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>)
OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task>)

O novo conjunto de APIs é o seguinte:

OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions AddDocumentTransformer<IOpenApiDocumentTransformer>()

OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer)
OpenApiOptions AddSchemaTransformer<IOpenApiSchemaTransformer>()

OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)
OpenApiOptions AddOperationTransformer<IOpenApiOperationTransformer>()

Microsoft.AspNetCore.OpenApi dá suporte ao corte e AOT nativo

O novo suporte OpenAPI integrado no ASP.NET Core agora também suporta corte e AOT nativo.

Introdução

Criar um novo projeto da API Web (AOT nativa) do ASP.NET Core.

dotnet new webapiaot

Adicione o pacote Microsoft.AspNetCore.OpenAPI.

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

Para esta versão prévia, você também precisa adicionar o pacote Microsoft.OpenAPI mais recente para evitar avisos de corte.

dotnet add package Microsoft.OpenApi

Atualize Program.cs para habilitar a geração de documentos OpenAPI.

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

Publique o aplicativo.

dotnet publish

O aplicativo publica usando AOT nativo sem avisos.

Suporte a chamadas ProducesProblem e ProducesValidationProblem em grupos de rotas

Os métodos de extensão ProducesProblem e ProducesValidationProblem foram atualizados para grupos de rotas. Esses métodos podem ser usados para indicar que todos os pontos de extremidade em um grupo de roteiros podem retornar respostas ProblemDetails ou ValidationProblemDetails para fins de metadados de OpenAPI.

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem(StatusCodes.Status500InternalServerError);

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, bool IsCompleted);

Problem e ValidationProblem os tipos de resultado suportam a construção com IEnumerable<KeyValuePair<string, object?>> valores

Antes do .NET 9, a construção dos tipos de resultado Problem e ValidationProblem em APIs mínimas exigiam que as propriedades errors e extensions fossem inicializadas com uma implementação de IDictionary<string, object?>. Nesta versão, essas APIs de construção oferecem suporte a sobrecargas que consomem IEnumerable<KeyValuePair<string, object?>>.

var app = WebApplication.Create();

app.MapGet("/", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions",
                                                       extensions: extensions);
});

Obrigado ao usuário do GitHub joegoldman2 por esta contribuição!

Autenticação e autorização

Esta seção descreve os novos recursos de autenticação e autorização.

OpenIdConnectHandler adiciona suporte para PAR (Solicitações de Autorização por Push)

Gostaríamos de agradecer a Joe DeCock da Duende Software por adicionar PAR (solicitações de autorização por push) ao OpenIdConnectHandler do ASP.NET Core. Joe descreveu o histórico e a motivação para habilitar o PAR em sua proposta de API da seguinte forma:

As solicitações de autorização por push (PAR) são um padrão OAuth relativamente novo que melhora a segurança dos fluxos OAuth e OIDC movendo os parâmetros de autorização do canal frontal para o canal traseiro. Ou seja, mover parâmetros de autorização de URLs de redirecionamento no navegador para direcionar chamadas http de máquina para máquina no back-end.

Isso impede que um invasor no navegador possa:

  • Ver parâmetros de autorização, que podem vazar PII.
  • Adulterar esses parâmetros, por exemplo, o invasor pode alterar o escopo do acesso solicitado.

Enviar os parâmetros de autorização também mantém as URLs de solicitação curtas. Os parâmetros de autorização podem ficar muito longos ao usar recursos OAuth e OIDC mais complexos, como solicitações de autorização avançadas. URLs longas causam problemas em muitos navegadores e infraestruturas de rede.

O uso do PAR é incentivado pelo grupo de trabalho FAPI dentro da OpenID Foundation. Por exemplo, o perfil de segurança FAPI2.0 requer o uso de PAR. Esse perfil de segurança é usado por muitos dos grupos que trabalham com open banking (principalmente na Europa), na área de saúde e em outros setores com altos requisitos de segurança.

O PAR é suportado por vários provedores de identity, incluindo

Para o .NET 9, decidimos habilitar o PAR por padrão se o documento de descoberta do provedor de identity anunciar suporte para PAR, pois ele deve fornecer segurança aprimorada para provedores que dão suporte a ele. O documento de descoberta do provedor de identity geralmente é encontrado em .well-known/openid-configuration. Se isso causar problemas, você poderá desabilitar o PAR por meio de OpenIdConnectOptions.PushedAuthorizationBehavior da seguinte maneira:

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });

Para garantir que a autenticação só seja bem-sucedida se o PAR for usado, use PushedAuthorizationBehavior.Require . Essa alteração também introduz um novo evento OnPushAuthorization para OpenIdConnectEvents que pode ser usado para personalizar a solicitação de autorização por push ou tratá-la manualmente. Veja Proposta de API para mais detalhes.

Personalização do parâmetro OIDC e OAuth

Os manipuladores de autenticação OAuth e OIDC agora têm uma opção AdditionalAuthorizationParameters para facilitar a personalização dos parâmetros de mensagem de autorização que geralmente são incluídos como parte da cadeia de caracteres de consulta de redirecionamento. No .NET 8 e anterior, isso requer um retorno de chamada personalizado OnRedirectToIdentityProvider ou um método substituído BuildChallengeUrl em um manipulador personalizado. Aqui está um exemplo de código .NET 8:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.SetParameter("prompt", "login");
        context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
        return Task.CompletedTask;
    };
});

O exemplo anterior agora pode ser simplificado para o seguinte código:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});

Configurar sinalizadores de autenticação estendida HTTP.sys

Agora você pode configurar os sinalizadores HTTP.sys HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING e HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL usando as novas propriedades EnableKerberosCredentialCaching e CaptureCredentials no HTTP.sys AuthenticationManager para otimizar o modo como a autenticação do Windows é tratada. Por exemplo:

webBuilder.UseHttpSys(options =>
{
    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    options.Authentication.EnableKerberosCredentialCaching = true;
    options.Authentication.CaptureCredentials = true;
});

Diversos

As seções a seguir descrevem diversos novos recursos.

Nova biblioteca de HybridCache

A API HybridCache abre algumas lacunas nas APIs IDistributedCache e IMemoryCache existentes. Ele também adiciona novos recursos, como:

  • Proteção "Stampede" para impedir buscas paralelas do mesmo trabalho.
  • Serialização configurável.

HybridCache foi projetado para ser uma substituição suspensa para uso de IDistributedCache e IMemoryCache existentes e fornece uma API simples para adicionar um novo código de cache. Ele fornece uma API unificada para cache em processo e fora do processo.

Para ver como a API HybridCache é simplificada, compare-a com o código que usa IDistributedCache. Aqui está um exemplo de como é usar IDistributedCache:

public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}

Isso é muito trabalho para acertar a cada vez, incluindo coisas como serialização. E no cenário de falha de cache, você pode acabar com vários threads simultâneos, todos recebendo uma falha de cache, todos buscando os dados subjacentes, todos serializando-os e todos enviando esses dados para o cache.

Para simplificar e melhorar esse código com HybridCache, primeiro precisamos adicionar a nova biblioteca Microsoft.Extensions.Caching.Hybrid:

<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />

Registre o serviço HybridCache, como você registraria uma implementação de IDistributedCache:

builder.Services.AddHybridCache(); // Not shown: optional configuration API.

Agora, a maioria das preocupações de cache pode ser descarregada para HybridCache:

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this combination.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}

Fornecemos uma implementação concreta da classe abstrata HybridCache por meio da injeção de dependência, mas a intenção é que os desenvolvedores possam fornecer implementações personalizadas da API. A implementação HybridCache lida com tudo relacionado ao cache, incluindo o tratamento de operações simultâneas. O token cancel aqui representa o cancelamento combinado de todos os chamadores simultâneos - não apenas o cancelamento do chamador que podemos ver (ou seja, token).

Cenários de alta taxa de transferência podem ser otimizados ainda mais usando o padrão TState, para evitar alguma sobrecarga de variáveis capturadas e retornos de chamada por instância:

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // unique key for this combination
            (name, id), // all of the state we need for the final call, if needed
            static async (state, token) =>
                await SomeExpensiveOperationAsync(state.name, state.id, token),
            token: token
        );
    }
}

HybridCache usa a implementação de IDistributedCache configurada, se houver, para o cache fora de processo secundário, por exemplo, usando o Redis. Mas mesmo sem um IDistributedCache, o serviço HybridCache ainda fornecerá proteção contra cache e "debandada" em processo.

Uma anotação sobre a reutilização do objeto

No código existente típico que usa IDistributedCache, cada recuperação de um objeto do cache resulta em desserialização. Esse comportamento significa que cada chamador simultâneo obtém uma instância separada do objeto, que não pode interagir com outras instâncias. O resultado é a segurança de thread, pois não há risco de modificações simultâneas na mesma instância de objeto.

Como muitos HybridCache uso serão adaptados do código IDistributedCache existente, HybridCache preserva esse comportamento por padrão para evitar a introdução de bugs de simultaneidade. No entanto, um determinado caso de uso é inerentemente thread-safe:

  • Se os tipos que estão sendo armazenados em cache forem imutáveis.
  • Se o código não os modificar.

Nesses casos, informe HybridCache que é seguro reutilizar instâncias:

  • Marcando o tipo como sealed. A palavra-chave sealed em C# significa que a classe não pode ser herdada.
  • Aplicando o atributo [ImmutableObject(true)] a ele. O atributo [ImmutableObject(true)] indica que o estado do objeto não pode ser alterado depois de criado.

Ao reutilizar instâncias, HybridCache pode reduzir a sobrecarga de alocações de CPU e objeto associadas à desserialização por chamada. Isso pode levar a melhorias de desempenho em cenários em que os objetos armazenados em cache são grandes ou acessados com frequência.

Outros recursos de HybridCache

Assim como IDistributedCache, HybridCache dá suporte à remoção por chave com um método RemoveKeyAsync.

HybridCache também fornece APIs opcionais para implementações de IDistributedCache, para evitar alocações byte[]. Esse recurso é implementado pelas versões prévias dos pacotes Microsoft.Extensions.Caching.StackExchangeRedis e Microsoft.Extensions.Caching.SqlServer.

A serialização é configurada como parte do registro do serviço, com suporte para serializadores específicos do tipo e generalizados por meio dos métodos WithSerializer e .WithSerializerFactory, encadeados da chamada AddHybridCache. Por padrão, a biblioteca manipula string e byte[] internamente e usa System.Text.Json para todo o resto, mas você pode usar protobuf, xml ou qualquer outra coisa.

HybridCache dá suporte a runtimes mais antigos do .NET, até .NET Framework 4.7.2 e .NET Standard 2.0.

Para obter mais informações sobre HybridCache, consulte Biblioteca HybridCache no ASP.NET Core

Melhorias na página de exceção do desenvolvedor

A página de exceção do desenvolvedor do ASP.NET Core é exibida quando um aplicativo lança uma exceção não tratada durante o desenvolvimento. A página de exceção do desenvolvedor fornece informações detalhadas sobre a exceção e a solicitação.

A versão prévia 3 adicionou metadados de ponto de extremidade à página de exceção do desenvolvedor. ASP.NET Core usa metadados de ponto de extremidade para controlar o comportamento do ponto de extremidade, como roteamento, cache de resposta, limitação de taxa, geração OpenAPI e muito mais. A imagem a seguir mostra as novas informações de metadados na seção Routing da página de exceção do desenvolvedor:

As novas informações de metadados na página de exceção do desenvolvedor

Durante o teste da página de exceção do desenvolvedor, foram identificados pequenos aprimoramentos de qualidade de vida. Eles foram enviados na versão prévia 4:

  • Melhor disposição de texto. Cookies longos, cadeia de caracteres de consulta e nomes de método não adicionam mais barras de rolagem horizontal do navegador.
  • Texto maior que é encontrado em designs modernos.
  • Tamanhos de tabela mais consistentes.

A imagem animada a seguir mostra a nova página de exceção do desenvolvedor:

A nova página de exceção do desenvolvedor

Melhorias de depuração do dicionário

A exibição de depuração de dicionários e outras coleções de chave-valor tem um layout aprimorado. A chave é exibida na coluna de chave do depurador em vez de ser concatenada com o valor. As imagens a seguir mostram a exibição antiga e nova de um dicionário no depurador.

Antes:

A experiência anterior do depurador

Depois:

A nova experiência do depurador

O ASP.NET Core tem muitas coleções de chave-valor. Essa experiência de depuração aprimorada se aplica a:

  • Cabeçalhos HTTP
  • Cadeias de consulta
  • Formulários
  • Cookies
  • Exibir dados
  • Rotear dados
  • Recursos

Correção para 503 durante o ciclo de aplicativo no IIS

Por padrão, agora há um atraso de 1 segundo entre quando o IIS é notificado de um ciclo ou desligamento e quando o ANCM informa ao servidor gerenciado para começar a desligar. O atraso é configurável por meio da variável de ambiente ANCM_shutdownDelay ou definindo a configuração do manipulador de shutdownDelay. Ambos os valores estão em milissegundos. O atraso é principalmente para reduzir a probabilidade de uma corrida em que:

  • O IIS não começou a enfileirar solicitações para ir para o novo aplicativo.
  • O ANCM começa a rejeitar novas solicitações que entram no aplicativo antigo.

Computadores ou computadores mais lentos com uso mais pesado da CPU podem querer ajustar esse valor para reduzir a probabilidade de 503.

Exemplo de configuração shutdownDelay:

<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
  <handlerSettings>
    <!-- Milliseconds to delay shutdown by.
    this doesn't mean incoming requests will be delayed by this amount,
    but the old app instance will start shutting down after this timeout occurs -->
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>

A correção está no módulo ANCM globalmente instalado que vem do pacote de hospedagem.

Otimizando a entrega de ativos da Web estáticos

Seguir as práticas recomendadas de produção para atender ativos estáticos requer uma quantidade significativa de conhecimento técnico e de trabalho. Sem otimizações como compressão, cache e impressões digitais:

  • O navegador precisa fazer solicitações adicionais em cada carregamento de página.
  • Mais bytes do que o necessário são transferidos pela rede.
  • Às vezes, versões desatualizadas de arquivos são fornecidas aos clientes.

A criação de aplicativos Web com bom desempenho requer a otimização do fornecimento de ativos ao navegador. As possíveis otimizações incluem:

  • Forneça um determinado ativo uma vez até que o arquivo seja alterado ou o navegador limpe seu cache. Definir o cabeçalho da ETag.
  • Impedir que o navegador use ativos antigos ou obsoletos depois que um aplicativo for atualizado. Definir o cabeçalho Last-Modified.
  • Configurar os cabeçalhos de cache adequadamente.
  • Use o middleware de cache.
  • Fornecer versões comprimidas dos ativos quando possível.
  • Use um CDN para fornecer os ativos mais próximos do usuário.
  • Minimize o tamanho dos ativos fornecidos ao navegador. Essa otimização não inclui a minificação.

MapStaticAssets é um novo middleware que ajuda a otimizar a entrega de ativos estáticos em um aplicativo. Ele foi projetado para funcionar com todas as estruturas de interface do usuário, incluindo Blazor, Razor Pages e MVC. Normalmente, é uma substituição imediata de UseStaticFiles:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();

app.Run();

MapStaticAssets opera combinando processos de build e de tempo de publicação para coletar informações sobre todos os recursos estáticos em um aplicativo. Essas informações são então utilizadas pela biblioteca de runtime para servir esses arquivos ao navegador com eficiência.

MapStaticAssets pode substituir UseStaticFiles na maioria das situações; no entanto, ele é otimizado para atender aos ativos dos quais o aplicativo tem conhecimento no tempo de compilação e publicação. Se o aplicativo atender ativos de outros locais, como disco ou recursos inseridos, o UseStaticFiles deverá ser usado.

MapStaticAssets fornece os seguintes benefícios não encontrados com UseStaticFiles:

  • Compactação de tempo de build para todos os ativos no aplicativo:
    • gzip durante o desenvolvimento e gzip + brotli durante a publicação.
    • Todos os ativos são compactados com a meta de reduzir o tamanho dos ativos ao mínimo.
  • ETags baseado em conteúdo: as Etags para cada recurso são a cadeia de caracteres codificada em Base64 do hash SHA-256 do conteúdo. Isso garante que o navegador só recarregue um arquivo se o conteúdo tiver sido alterado.

A tabela a seguir mostra os tamanhos originais e compactados do CSS e dos arquivos JS no modelo padrão do Razor Pages:

Arquivo Original Compressed Redução de %
bootstrap.min.css 163 17.5 89,26%
jquery.js 89,6 28 68,75%
bootstrap.min.js 78,5 20 74,52%
Total 331,1 65,5 80,20%

A tabela a seguir mostra os tamanhos originais e compactados usando a Biblioteca de componentes Blazor da interface do usuário do Fluent:

Arquivo Original Compressed Redução de %
fluent.js 384 73 80.99%
fluent.css 94 11 88,30%
Total 478 84 82,43%

Para um total de 478 KB descompactados a 84 KB compactados.

A tabela a seguir mostra os tamanhos originais e compactados usando a biblioteca de componentes MudBlazorBlazor:

Arquivo Original Compressed Redução
MudBlazor.min.css 541 37,5 93,07%
MudBlazor.min.js 47.4 9.2 80,59%
Total 588,4 46,7 92,07%

A otimização ocorre automaticamente ao usar MapStaticAssets. Quando uma biblioteca é adicionada ou atualizada, por exemplo, com o novo JavaScript ou CSS, os ativos são otimizados como parte do build. A otimização é especialmente benéfica para ambientes móveis que podem ter uma largura de banda menor ou conexões não confiáveis.

Para obter mais informações sobre os novos recursos de entrega de arquivos, consulte os seguintes recursos:

Habilitar a compactação dinâmica no servidor versus usar MapStaticAssets

MapStaticAssets tem as seguintes vantagens em relação à compactação dinâmica no servidor:

  • É mais simples porque não há nenhuma configuração específica do servidor.
  • É mais eficaz porque os ativos são compactados no momento da compilação.
  • Permite que o desenvolvedor gaste tempo extra durante o processo de build para garantir que os ativos sejam do tamanho mínimo.

Considere a tabela a seguir comparando a compactação MudBlazor com a compactação dinâmica do IIS e MapStaticAssets:

Gzip do IIS MapStaticAssets Redução de MapStaticAssets
≅ 90 37,5 59%

ASP0026: analisador para avisar quando [Authorize] é substituído por [AllowAnonymous] “mais distante”

Parece intuitivo que um atributo [Authorize] colocado “mais próximo” de uma ação MVC do que um atributo [AllowAnonymous] substituiria o atributo [AllowAnonymous] e forçaria a autorização. No entanto, isso não é necessariamente o caso. O que importa é a ordem relativa dos atributos.

O código a seguir mostra exemplos em que um atributo [Authorize] mais próximo é substituído por um atributo [AllowAnonymous] que está mais distante.

[AllowAnonymous]
public class MyController
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on the class
    public IActionResult Private() => null;
}
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
    public IActionResult Private() => null;
}
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}

Na versão prévia 6 do .NET 9, introduzimos um analisador que realçará instâncias como essas em que um atributo [Authorize] mais próximo é substituído por um atributo [AllowAnonymous] mais distante de uma ação do MVC. O aviso aponta para o atributo [Authorize] substituído com a seguinte mensagem:

ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away

A ação correta a ser tomada se você vir esse aviso depende da intenção por trás dos atributos. O atributo [AllowAnonymous] mais distante deverá ser removido se estiver expondo involuntariamente o ponto de extremidade a usuários anônimos. Se o atributo [AllowAnonymous] tiver a intenção de substituir um atributo [Authorize] mais próximo, você poderá repetir o atributo [AllowAnonymous] após o atributo [Authorize] para esclarecer a intenção.

[AllowAnonymous]
public class MyController
{
    // This produces no warning because the second, "closer" [AllowAnonymous]
    // clarifies that [Authorize] is intentionally overridden.
    // Specifying AuthenticationSchemes can still be useful
    // for endpoints that allow but don't require authenticated users.
    [Authorize(AuthenticationSchemes = "Cookies")]
    [AllowAnonymous]
    public IActionResult Privacy() => null;
}

Métricas de conexão Kestrel aprimoradas

Fizemos uma melhoria significativa nas métricas de conexão Kestrel incluindo metadados sobre o motivo da falha de uma conexão. A kestrel.connection.duration métrica agora inclui o motivo do fechamento da conexão no error.type atributo.

Aqui está uma pequena amostra dos error.type valores:

  • tls_handshake_failed - A conexão requer TLS e o handshake TLS falhou.
  • connection_reset - A conexão foi fechada inesperadamente pelo cliente enquanto as requisições estavam em andamento.
  • request_headers_timeout - Kestrel fechou a conexão porque ela não recebeu cabeçalhos da requisição a tempo.
  • max_request_body_size_exceeded - Kestrel Fechou a conexão porque os dados enviados excederam o tamanho máximo.

Anteriormente, o diagnóstico de problemas de Kestrel conexão exigia que um servidor gravasse registros detalhados e de baixo nível. No entanto, os logs podem ser caros para gerar e armazenar, e pode ser difícil encontrar as informações desejadas em meio ao ruído.

As métricas são uma alternativa muito mais barata que pode ser deixada em um ambiente de produção com impacto mínimo. As métricas coletadas podem gerar painéis e alertas. Assim que um problema é identificado em alto nível com métricas, uma investigação mais aprofundada usando o registro em log e outras ferramentas pode começar.

Esperamos que as métricas de conexão aprimoradas sejam úteis em muitos cenários:

  • Investigar problemas de desempenho causados por conexões com tempos de vida curtos.
  • Observar ataques externos contínuos a Kestrel esse impacto no desempenho e na estabilidade.
  • Gravação de tentativas de ataques externos em Kestrel que o fortalecimento de segurança embutido Kestrel impediu.

Para saber mais, confira Métricas do ASP.NET Core

Personalizar Kestrel pontos de extremidade de pipe nomeados

O suporte ao pipe nomeado do Kestrel foi aprimorado com opções avançadas de personalização. O novo CreateNamedPipeServerStream método nas opções de pipe nomeado permite que os pipes sejam personalizados por pontos de extremidade.

Um exemplo de onde isso é útil é um Kestrel aplicativo que requer dois pontos de extremidade de pipe com segurança de acesso diferentes. A CreateNamedPipeServerStream opção pode ser usada para criar pipes com configurações de segurança personalizadas, dependendo do nome do pipe.

var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("pipe1");
    options.ListenNamedPipe("pipe2");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndpoint.PipeName);

        return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
            NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
            context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
    };
});

ExceptionHandlerMiddleware para escolher o código de status com base no tipo de exceção

Uma nova opção ao configurar o ExceptionHandlerMiddleware permite que os desenvolvedores de aplicativos escolham qual código de status retornar quando ocorrer uma exceção durante o tratamento de solicitações. A nova opção altera o código de status que está sendo definido na ProblemDetailsresposta do ExceptionHandlerMiddleware.

app.UseExceptionHandler(new ExceptionHandlerOptions
{
    StatusCodeSelector = ex => ex is TimeoutException
        ? StatusCodes.Status503ServiceUnavailable
        : StatusCodes.Status500InternalServerError,
});

Recusar métricas HTTP em determinados pontos de extremidade e solicitações

O .NET 9 apresenta a capacidade de recusar métricas HTTP para pontos de extremidade e solicitações específicos. A recusa de registro de métricas é benéfica para os pontos de extremidade frequentemente chamados por sistemas automatizados, como verificações de integridade. O registro de métricas para essas solicitações geralmente é desnecessário.

As solicitações HTTP para um ponto de extremidade podem ser excluídas das métricas adicionando metadados. Qualquer um:

  • Adicione o [DisableHttpMetrics] atributo ao controlador da API Web, ao SignalR hub ou ao serviço gRPC.
  • Chame DisableHttpMetrics ao mapear pontos de extremidade na inicialização do aplicativo:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();

A MetricsDisabledpropriedade foi adicionada a IHttpMetricsTagsFeature.

  • Cenários avançados onde uma solicitação não é mapeada para um ponto de extremidade.
  • Desabilitar dinamicamente a coleta de métricas para solicitações HTTP específicas.
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
    var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (metricsFeature != null &&
        context.Request.Headers.ContainsKey("x-disable-metrics"))
    {
        metricsFeature.MetricsDisabled = true;
    }

    await next(context);
});

Suporte à proteção de dados para exclusão de chaves

Antes do .NET 9, as chaves de proteção de dados não podiam ser excluídas por design, para evitar a perda de dados. A exclusão de uma chave torna seus dados protegidos irrecuperáveis. Dado seu pequeno tamanho, o acúmulo dessas chaves geralmente representava um impacto mínimo. No entanto, para acomodar serviços de execução extremamente longa, introduzimos a opção de excluir chaves. Geralmente, apenas chaves antigas devem ser excluídas. Exclua as chaves apenas quando puder aceitar o risco de perda de dados em troca de economia de armazenamento. Recomendamos que as chaves de proteção de dados não sejam excluídas.

using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();
services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)
{
    var utcNow = DateTimeOffset.UtcNow;
    var yearAgo = utcNow.AddYears(-1);

    if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
    {
        Console.WriteLine("Failed to delete keys.");
    }
    else
    {
        Console.WriteLine("Old keys deleted successfully.");
    }
}
else
{
    Console.WriteLine("Key manager does not support deletion.");
}

O middleware aceita DI com chave

O middleware agora aceita a ID com chave no construtor e no método Invoke/InvokeAsync:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("test")] MySingletonClass service)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("test2")]
            MyScopedClass scopedService) => _next(context);
}

Confiar no certificado de desenvolvimento HTTPS do ASP.NET Core no Linux

Em distribuições do Linux baseadas em Ubuntu e Fedora, dotnet dev-certs https --trust agora configura certificado de desenvolvimento HTTPS do ASP.NET Core como um certificado confiável para:

  • Navegadores Chromium, por exemplo, Google Chrome, Microsoft Edge e Chromium.
  • Mozilla Firefox e navegadores derivados do Mozilla.
  • APIs .NET, por exemplo, HttpClient

Anteriormente, --trust funcionava apenas no Windows e no macOS. A confiança do certificado é aplicada por usuário.

Para estabelecer confiança no OpenSSL, a ferramenta dev-certs:

  • Coloca o certificado em ~/.aspnet/dev-certs/trust
  • Executa uma versão simplificada da ferramenta c_rehash do OpenSSL no diretório.
  • Pede ao usuário para atualizar a variável de ambiente SSL_CERT_DIR.

Para estabelecer confiança em dotnet, a ferramenta coloca o certificado no repositório de certificados My/Root.

Para estabelecer confiança em bancos de dados NSS, se houver, a ferramenta pesquisa no diretório home por perfis do Firefox, ~/.pki/nssdb e ~/snap/chromium/current/.pki/nssdb. Para cada diretório encontrado, a ferramenta adiciona uma entrada a nssdb.