Compartilhar via


Novidades no ASP.NET Core no .NET 10

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

Este artigo será atualizado à medida que novas versões prévias forem disponibilizadas. Para alterações significativas, consulte Alterações significativas no .NET.

Blazor

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

Exemplos de segurança novos e atualizados Blazor Web App

Adicionamos e atualizamos os Blazor Web App exemplos de segurança vinculados nos seguintes artigos:

Todas as nossas soluções de exemplo OIDC e Entra agora incluem um projeto de API Web separado (MinimalApiJwt) para demonstrar como configurar e chamar uma API Web externa com segurança. O uso de APIs Web é demonstrado com um manipulador de token e um cliente HTTP nomeado para o provedor de identidade OIDC ou para pacotes/API Web da Microsoft Entra ID.

As soluções de exemplo são configuradas no código C# nos arquivos Program. Para configurar as soluções a partir de arquivos de configurações de aplicativo (por exemplo, appsettings.json), consulte a seção nova sobre a configuração de fornecimento utilizando o provedor de configuração JSON (configurações de aplicativo) nos artigos OIDC ou Entra.

Nossos exemplos do Entra também incluem novas diretrizes sobre como usar um cache de token distribuído criptografado para cenários de hospedagem de farm da Web.

parâmetro QuickGridRowClass

Aplique uma classe de estilo a uma linha da grade com base no item da linha usando o novo parâmetro RowClass. No exemplo a seguir, o método GetRowCssClass é chamado em cada linha para aplicar condicionalmente uma classe de folha de estilos com base no item de linha:

<QuickGrid ... RowClass="GetRowCssClass">
    ...
</QuickGrid>

@code {
    private string GetRowCssClass(MyGridItem item) =>
        item.IsArchived ? "row-archived" : null;
}

Para obter mais informações, consulte o componente `Blazor` do ASP.NET CoreQuickGrid.

Blazor script como recurso web estático

Em versões anteriores do .NET, o script Blazor é fornecido de um recurso inserido na estrutura compartilhada do ASP.NET Core. No .NET 10 ou posterior, o script Blazor é servido como um ativo da Web estático com compactação automática e impressão digital.

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

Destaques do esquema de rota

O [Route] atributo agora dá suporte ao realce de sintaxe de rota para ajudar a visualizar a estrutura do modelo de rota:

O padrão de modelo de rota de um atributo de rota para o valor do contador mostra o realce da sintaxe

Anteriormente, NavigationManager.NavigateTo rolava até a parte superior da página para navegação na mesma página. Esse comportamento foi alterado no .NET 10 para que o navegador não role mais para a parte superior da página ao navegar para a mesma página. Isso significa que o viewport não é mais redefinido ao fazer atualizações no endereço da página atual, como ao alterar a string de consulta ou o fragmento.

Componente de IU de reconexão adicionado ao modelo de projeto Blazor Web App

O modelo de projeto Blazor Web App agora inclui um componente ReconnectModal, incluindo arquivos JavaScript e folhas de estilos agrupados, para melhorar o controle do desenvolvedor sobre a IU de reconexão quando o cliente perde a conexão WebSocket com o servidor. O componente não insere estilos programaticamente, garantindo a conformidade com configurações de CSP (Política de Segurança de Conteúdo) mais rigorosas para a política de style-src. Em versões anteriores, a interface do usuário de reconexão padrão foi criada pela estrutura de uma maneira que poderia causar violações de CSP. Observe que a interface do usuário de reconexão padrão ainda é usada como alternativa quando o aplicativo não define a interface do usuário de reconexão, como ao usar o componente ReconnectModal do modelo de projeto ou um componente personalizado semelhante.

Novos recursos de IU de reconexão:

  • Além de indicar o estado de reconexão definindo uma classe CSS específica no elemento de interface de reconexão, o novo evento components-reconnect-state-changed é disparado para alterações de estado de reconexão.
  • O código pode diferenciar melhor os estágios do processo de reconexão com o novo estado de reconexão "retrying", indicado pela classe CSS e pelo novo evento.

Para obter mais informações, consulte as diretrizes do ASP.NET CoreBlazorSignalR.

Ignorar a cadeia de caracteres e o fragmento de consulta ao usar NavLinkMatch.All

O componente NavLink agora ignora a cadeia de caracteres de consulta e o fragmento ao usar o valor NavLinkMatch.All para o parâmetro Match. Isso significa que o link mantém a classe active se o caminho da URL corresponder, mas a cadeia de caracteres de consulta ou o fragmento mudarem. Para reverter para o comportamento original, use o conjunto de Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragmentAppContext opções como true.

Você também pode substituir o método ShouldMatch em NavLink para personalizar o comportamento correspondente:

public class CustomNavLink : NavLink
{
    protected override bool ShouldMatch(string currentUriAbsolute)
    {
        // Custom matching logic
    }
}

Para obter mais informações, consulte ASP.NET Core roteamento e navegaçãoBlazor.

Fechar opções de coluna QuickGrid

Agora você pode fechar a UI das opções da coluna QuickGrid usando o novo método HideColumnOptionsAsync.

O exemplo a seguir usa o método HideColumnOptionsAsync para fechar a interface do usuário das opções de coluna assim que o filtro de título é aplicado:

<QuickGrid @ref="movieGrid" Items="movies">
    <PropertyColumn Property="@(m => m.Title)" Title="Title">
        <ColumnOptions>
            <input type="search" @bind="titleFilter" placeholder="Filter by title" 
                @bind:after="@(() => movieGrid.HideColumnOptionsAsync())" />
        </ColumnOptions>
    </PropertyColumn>
    <PropertyColumn Property="@(m => m.Genre)" Title="Genre" />
    <PropertyColumn Property="@(m => m.ReleaseYear)" Title="Release Year" />
</QuickGrid>

@code {
    private QuickGrid<Movie>? movieGrid;
    private string titleFilter = string.Empty;
    private IQueryable<Movie> movies = new List<Movie> { ... }.AsQueryable();
    private IQueryable<Movie> filteredMovies => 
        movies.Where(m => m.Title!.Contains(titleFilter));
}

O streaming de resposta é de adesão opcional e como optar por não participar

Em versões anteriores de Blazor, o streaming de resposta para solicitações HttpClient era opcional. Agora, o streaming de resposta está habilitado por padrão.

Essa é uma alteração significativa porque a chamada de HttpContent.ReadAsStreamAsync para um HttpResponseMessage.Content (response.Content.ReadAsStreamAsync()) retorna um BrowserHttpReadStream e não mais um MemoryStream. BrowserHttpReadStream não dá suporte a operações síncronas, como Stream.Read(Span<Byte>). Se o código usar operações síncronas, você poderá optar por não usar o streaming de resposta ou copiar o Stream para um MemoryStream você mesmo.

Para recusar o streaming de resposta globalmente, use uma das seguintes abordagens:

  • Adicione a <WasmEnableStreamingResponse> propriedade ao arquivo de projeto com um valor de false:

    <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
    
  • Defina a variável de DOTNET_WASM_ENABLE_STREAMING_RESPONSE ambiente como false ou 0.

Para desativar o streaming de resposta para uma solicitação individual, defina SetBrowserResponseStreamingEnabled como false no HttpRequestMessage (requestMessage no exemplo a seguir).

requestMessage.SetBrowserResponseStreamingEnabled(false);

Para obter mais informações, consulte HttpClient e HttpRequestMessage com as opções de solicitação da Fetch API (artigo 'Chamar API Web').

Impressão digital do lado do cliente

No ano passado, o lançamento do .NET 9 introduziu a impressão digital do lado do servidor de ativos estáticos em Blazor Web Apps com a introdução das convenções de ponto de extremidade de roteamento de Ativos Estáticos do Mapa (MapStaticAssets), o ImportMap componente e a ComponentBase.Assets propriedade (@Assets["..."]) para resolver módulos JavaScript com impressão digital. Para o .NET 10, você pode optar por ativar a impressão digital feita no lado do cliente de módulos JavaScript para aplicativos independentes Blazor WebAssembly.

Em aplicativos Blazor WebAssembly autônomos, durante a compilação/publicação, a estrutura substitui espaços reservados em index.html por valores calculados durante a compilação para ativos estáticos de impressão digital. Uma impressão digital é colocada no nome do arquivo de script blazor.webassembly.js.

A marcação a seguir deve estar presente no arquivo wwwwoot/index.html para adotar o recurso de impressão digital.

<head>
    ...
+   <script type="importmap"></script>
</head>

<body>
    ...
-   <script src="_framework/blazor.webassembly.js"></script>
+   <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>

</html>

No arquivo de projeto (.csproj), adicione a propriedade <OverrideHtmlAssetPlaceholders> definida como true:

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

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
+   <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>
</Project>

Qualquer script em index.html com o marcador de impressão digital é identificado pela estrutura. Por exemplo, um arquivo de script nomeado scripts.js na pasta wwwroot/js do aplicativo é registrado ao se adicionar #[.{fingerprint}] antes da extensão do arquivo (.js).

<script src="js/scripts#[.{fingerprint}].js"></script>

Para identificar módulos adicionais JS em aplicativos independentes Blazor WebAssembly, use a propriedade <StaticWebAssetFingerprintPattern> no arquivo de projeto do aplicativo (.csproj).

No exemplo a seguir, uma impressão digital é adicionada para todos os arquivos fornecidos pelo .mjs desenvolvedor no aplicativo:

<StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs" 
  Expression="#[.{fingerprint}]!" />

Os arquivos são colocados automaticamente no mapa de importação:

  • Automaticamente para CSR Blazor Web App.
  • Ao optar pela impressão digital do módulo em aplicativos autônomos Blazor WebAssembly de acordo com as instruções anteriores.

Ao resolver a importação para a interoperabilidade do JavaScript, o mapa de importação é usado pelo navegador para resolver arquivos com impressão digital.

Definir o ambiente em aplicativos autônomos Blazor WebAssembly

O Properties/launchSettings.json arquivo não é mais usado para controlar o ambiente em aplicativos autônomos Blazor WebAssembly .

A partir do .NET 10, defina o ambiente com a <WasmApplicationEnvironmentName> propriedade no arquivo de projeto do aplicativo (.csproj).

O exemplo a seguir define o ambiente do aplicativo como Staging:

<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>

Os ambientes padrão são:

  • Development para compilação.
  • Production para publicação.

Arquivo de configuração de boot integrado

A configuração de inicialização do Blazor, que antes do lançamento do .NET 10 existia em um arquivo chamado blazor.boot.json, foi embutida no script do dotnet.js. Isso afeta apenas os desenvolvedores que estão interagindo diretamente com o blazor.boot.json arquivo, como quando os desenvolvedores estão:

Atualmente, não há nenhuma estratégia de substituição documentada para as abordagens anteriores. Se você precisar de uma das estratégias anteriores, abra um novo problema de documentação descrevendo seu cenário usando o link Abrir um problema de documentação na parte inferior de qualquer artigo.

Modelo declarativo para manter o estado de componentes e serviços

Agora você pode especificar declarativamente o estado para persistir em componentes e serviços usando o atributo [SupplyParameterFromPersistentComponentState]. As propriedades com esse atributo são mantidas automaticamente usando o PersistentComponentState serviço durante a pré-geração. O estado é recuperado quando o componente é renderizado interativamente ou o serviço é instanciado.

Nas versões anteriores Blazor, manter o estado do componente durante a pré-geração usando o serviço PersistentComponentState envolvia uma quantidade significativa de código, como demonstra o exemplo a seguir:

@page "/movies"
@implements IDisposable
@inject IMovieService MovieService
@inject PersistentComponentState ApplicationState

@if (MoviesList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <QuickGrid Items="MoviesList.AsQueryable()">
        ...
    </QuickGrid>
}

@code {
    public List<Movie>? MoviesList { get; set; }
    private PersistingComponentStateSubscription? persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<List<Movie>>(nameof(MoviesList), 
            out var movies))
        {
            MoviesList = await MovieService.GetMoviesAsync();
        }
        else
        {
            MoviesList = movies;
        }

        persistingSubscription = ApplicationState.RegisterOnPersisting(() =>
        {
            ApplicationState.PersistAsJson(nameof(MoviesList), MoviesList);
            return Task.CompletedTask;
        });
    }

    public void Dispose() => persistingSubscription?.Dispose();
}

Esse código agora pode ser simplificado usando o novo modelo declarativo:

@page "/movies"
@inject IMovieService MovieService

@if (MoviesList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <QuickGrid Items="MoviesList.AsQueryable()">
        ...
    </QuickGrid>
}

@code {
    [SupplyParameterFromPersistentComponentState]
    public List<Movie>? MoviesList { get; set; }

    protected override async Task OnInitializedAsync()
    {
        MoviesList ??= await MovieService.GetMoviesAsync();
    }
}

O estado pode ser serializado para vários componentes do mesmo tipo e você pode estabelecer um estado declarativo em um serviço para uso no aplicativo invocando RegisterPersistentService no Razor construtor de componentes (AddRazorComponents) com um tipo de serviço personalizado e modo de renderização. Para obter mais informações, consulte Os componentes do Prerender ASP.NET CoreRazor.

Novos recursos de interoperabilidade do JavaScript

Blazor adiciona suporte para os seguintes JS recursos de interoperabilidade:

  • Crie uma instância de um JS objeto usando uma função de construtor e obtenha o IJSObjectReference/IJSInProcessObjectReference identificador .NET para referenciar a instância.
  • Leia ou modifique o valor de uma propriedade de objeto JS, incluindo propriedades de dados e de acesso.

Os seguintes métodos assíncronos estão disponíveis no IJSRuntime e IJSObjectReference com o mesmo comportamento de escopo que o método existente IJSRuntime.InvokeAsync:

  • InvokeNewAsync(string identifier, object?[]? args): invoca a função de construtor especificada JS de forma assíncrona. A função é invocada com o new operador. No exemplo a seguir, jsInterop.TestClass é uma classe com uma função de construtor e classRef é um IJSObjectReference:

    var classRef = await JSRuntime.InvokeNewAsync("jsInterop.TestClass", "Blazor!");
    var text = await classRef.GetValueAsync<string>("text");
    var textLength = await classRef.InvokeAsync<int>("getTextLength");
    
  • GetValueAsync<TValue>(string identifier): lê o valor da propriedade especificada JS de forma assíncrona. A propriedade não pode ser somente uma setpropriedade. Um JSException será gerado se a propriedade não existir. O exemplo a seguir retorna um valor de uma propriedade de dados:

    var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>(
      "jsInterop.testObject.num");
    
  • SetValueAsync<TValue>(string identifier, TValue value): atualiza o valor da propriedade especificada JS de forma assíncrona. A propriedade não pode ser somente uma getpropriedade. Se a propriedade não estiver definida no objeto de destino, a propriedade será criada. Um JSException será gerado se a propriedade existir, mas não for gravável ou quando uma nova propriedade não puder ser adicionada ao objeto. No exemplo a seguir, num será criado em testObject com um valor de 30 se ele não existir.

    await JSRuntime.SetValueAsync("jsInterop.testObject.num", 30);
    

Sobrecargas estão disponíveis para cada um dos métodos mencionados, que aceitam um argumento CancellationToken ou um argumento de tempo limite TimeSpan.

Os seguintes métodos síncronos estão disponíveis em IJSInProcessRuntime e IJSInProcessObjectReference com o mesmo comportamento de escopo que o método existente IJSInProcessObjectReference.Invoke:

  • InvokeNew(string identifier, object?[]? args): invoca a função de construtor especificada JS de forma síncrona. A função é invocada com o new operador. No exemplo a seguir, jsInterop.TestClass é uma classe com uma função de construtor e classRef é um IJSInProcessObjectReference:

    var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
    var classRef = inProcRuntime.InvokeNew("jsInterop.TestClass", "Blazor!");
    var text = classRef.GetValue<string>("text");
    var textLength = classRef.Invoke<int>("getTextLength");
    
  • GetValue<TValue>(string identifier): lê o valor da propriedade especificada JS de forma síncrona. A propriedade não pode ser somente uma setpropriedade. Um JSException será gerado se a propriedade não existir. O exemplo a seguir retorna um valor de uma propriedade de dados:

    var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
    var valueFromDataProperty = inProcRuntime.GetValue<int>(
      "jsInterop.testObject.num");
    
  • SetValue<TValue>(string identifier, TValue value): atualiza o valor da propriedade especificada JS de forma síncrona. A propriedade não pode ser somente uma getpropriedade. Se a propriedade não estiver definida no objeto de destino, a propriedade será criada. Um JSException será gerado se a propriedade existir, mas não for gravável ou quando uma nova propriedade não puder ser adicionada ao objeto. No exemplo a seguir, num será criado em testObject com um valor de 20 se ele não existir.

    var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
    inProcRuntime.SetValue("jsInterop.testObject.num", 20);
    

Para obter mais informações, consulte as seções a seguir do artigo Chamar funções JavaScript de métodos .NET:

Blazor WebAssembly perfilamento de desempenho e contadores de diagnóstico

Novos contadores de perfil de desempenho e diagnóstico estão disponíveis para Blazor WebAssembly aplicativos. Para obter mais informações, consulte os seguintes artigos:

Ativos estáticos do framework pré-carregados Blazor

Em Blazor Web Apps, os ativos estáticos da estrutura são automaticamente pré-carregados usando Link cabeçalhos, o que permite ao navegador pré-carregar recursos antes que a página inicial seja buscada e renderizada. Em aplicativos independentes Blazor WebAssembly, os ativos do framework são agendados para download e cache com alta prioridade no início do processamento de páginas no navegador index.html.

Saiba mais em Arquivos estáticos de Blazor no ASP.NET Core.

Anteriormente, chamar NavigationManager.NavigateTo durante a renderização estática do lado do servidor (SSR) lançaria um erro NavigationException, interrompendo a execução antes de ser convertido em uma resposta de redirecionamento. Isso causou confusão durante a depuração e era inconsistente com a renderização interativa, em que o código após NavigateTo continua a ser executado normalmente.

A chamada NavigationManager.NavigateTo durante o SSR estático não lança mais um NavigationException. Em vez disso, ele se comporta de forma consistente com a renderização interativa ao executar a navegação sem gerar uma exceção.

O código que dependia do lançamento de NavigationException deve ser atualizado. Por exemplo, na interface do usuário padrão BlazorIdentity, o IdentityRedirectManager anteriormente lançava uma InvalidOperationException após chamar RedirectTo para garantir que não fosse invocado durante a renderização interativa. Essa exceção e os [DoesNotReturn] atributos agora devem ser removidos.

Para reverter para o comportamento anterior de gerar um NavigationException, defina a seguinte opção AppContext:

AppContext.SetSwitch(
    "Microsoft.AspNetCore.Components.Endpoints.NavigationManager.EnableThrowNavigationException", 
    isEnabled: true);

Respostas de 'Não Encontrado' usando NavigationManager para SSR estático e renderização interativa global

Agora NavigationManager inclui um NotFound método para lidar com cenários em que um recurso solicitado não é encontrado durante a renderização estática do lado do servidor (SSR estático) ou a renderização interativa global:

  • Renderização estática do lado do servidor (SSR estático): ao chamar NotFound, define o código de status HTTP como 404.
  • Renderização de streaming: gera uma exceção se a resposta já tiver sido iniciada.
  • Renderização interativa: sinaliza o Blazor roteador (Router componente) para renderizar o conteúdo não encontrado.

O suporte à renderização por página/componente está planejado para a Versão Prévia 5 em junho de 2025.

Você pode usar o NavigationManager.OnNotFound evento para notificações quando NotFound é invocado.

Para obter mais informações e exemplos, consulte ASP.NET Roteamento e navegação principaisBlazor.

Blazor o roteador tem um NotFoundPage parâmetro

Blazor agora fornece uma maneira melhorada de exibir uma página "Não Encontrado" quando tenta acessar uma página que não existe. Você pode especificar uma página a ser renderizada ao NavigationManager.NotFound passar um tipo de página para o Router componente usando o parâmetro NotFoundPage. Essa abordagem é recomendada em relação ao fragmento NotFound anterior, pois oferece suporte ao roteamento e funciona através de middleware de re-execução de código, sendo compatível mesmo com cenários diferentes de Blazor. Se um fragmento NotFound e NotFoundPage estiverem definidos, a página especificada por NotFoundPage tem prioridade.

<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>This content is ignored because NotFoundPage is defined.</NotFound>
</Router>

O Blazor modelo de projeto agora inclui uma NotFound.razor página por padrão. Essa página é renderizada automaticamente sempre que NavigationManager.NotFound é chamada em seu aplicativo, facilitando o processamento de rotas ausentes com uma experiência de usuário consistente.

Para obter mais informações, consulte ASP.NET Core roteamento e navegaçãoBlazor.

Métricas e rastreamento

Esta versão apresenta métricas abrangentes e recursos de rastreamento para Blazor aplicativos, fornecendo observabilidade detalhada do ciclo de vida do componente, navegação, manipulação de eventos e gerenciamento de circuitos.

Para obter mais informações, confira Melhores Práticas do ASP.NET CoreBlazor.

Blazor Hybrid

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

Novo .NET MAUIBlazor Hybrid com um artigo e um exemplo do Blazor Web App e do ASP.NET Core Identity

Um novo artigo e um aplicativo de exemplo foram adicionados para .NET MAUIBlazor Hybrid e Aplicativo Web usando ASP.NET Core Identity.

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

SignalR

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

APIs mínimas

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

Tratar a cadeia de caracteres vazia em postagens de formulário como nulo para tipos de valor que permitem valor nulo

Ao usar o atributo [FromForm] com um objeto complexo em APIs mínimas, valores de cadeias de caracteres vazias em uma postagem de formulário agora são convertidos em null em vez de causar uma falha de análise. Esse comportamento corresponde à lógica de processamento de postagens de formulário não associadas a objetos complexos em APIs mínimas.

using Microsoft.AspNetCore.Http;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/todo", ([FromForm] Todo todo) => TypedResults.Ok(todo));

app.Run();

public class Todo
{
  public int Id { get; set; }
  public DateOnly? DueDate { get; set; } // Empty strings map to `null`
  public string Title { get; set; }
  public bool IsCompleted { get; set; }
}

Obrigado a @nvmkpk por contribuir com essa mudança!

Suporte à validação em APIs mínimas

O suporte para validação em APIs Mínimas agora está disponível. Esse recurso permite que você solicite a validação dos dados enviados para seus endpoints da API. Habilitar a validação permite que o runtime do ASP.NET Core execute as validações definidas no:

  • Consulta
  • Cabeçalho
  • Corpo da solicitação

As validações são definidas usando atributos no DataAnnotations namespace. Os desenvolvedores personalizam o comportamento do sistema de validação:

  • Criando implementações de atributo personalizado [Validation] .
  • Implementando a IValidatableObject interface para lógica de validação complexa.

Se a validação falhar, o runtime retornará uma resposta 400 Solicitação incorreta com detalhes dos erros de validação.

Habilitar o suporte de validação interna para APIs mínimas

Habilite o suporte de validação interna para APIs mínimas chamando o AddValidation método de extensão para registrar os serviços necessários no contêiner de serviço para seu aplicativo:

builder.Services.AddValidation();

A implementação descobre automaticamente tipos definidos em manipuladores mínimos de API ou como tipos base de tipos definidos em manipuladores mínimos de API. Um filtro de ponto de extremidade executa a validação nesses tipos e é adicionado para cada ponto de extremidade.

A validação pode ser desabilitada para pontos de extremidade específicos usando o DisableValidation método de extensão, como no exemplo a seguir:

app.MapPost("/products",
    ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
        => TypedResults.Ok(productId))
    .DisableValidation();

Observação

Foram feitas várias pequenas melhorias e correções no gerador de validação de APIs mínimas introduzido no ASP.NET Core para .NET 10. Para dar suporte a aprimoramentos futuros, as APIs de resolvedor de validação subjacente agora são marcadas como experimentais. As APIs de nível AddValidation superior e o filtro de validação interno permanecem estáveis e não experimentais.

Validação com tipos de registro

APIs mínimas também dão suporte à validação com tipos de registro C#. Os tipos de registro podem ser validados usando atributos do System.ComponentModel.DataAnnotations namespace, semelhantes às classes. Por exemplo:

public record Product(
    [Required] string Name,
    [Range(1, 1000)] int Quantity);

Ao usar tipos de registro como parâmetros em endpoints da API Minimal, os atributos de validação são aplicados automaticamente da mesma maneira que os tipos de classe:

app.MapPost("/products", (Product product) =>
{
    // Endpoint logic here
    return TypedResults.Ok(product);
});

Suporte para eventos enviados do servidor (SSE)

ASP.NET Core agora dá suporte ao retorno de um resultado ServerSentEvents usando a API TypedResults.ServerSentEvents . Esse recurso tem suporte em APIs mínimas e aplicativos baseados em controlador.

Server-Sent Eventos é uma tecnologia de push de servidor que permite que um servidor envie um fluxo de mensagens de evento para um cliente por meio de uma única conexão HTTP. No .NET, as mensagens de evento são representadas como SseItem<T> objetos, que podem conter um tipo de evento, uma ID e um conteúdo de dados do tipo T.

A classe TypedResults tem um novo método estático chamado ServerSentEvents que pode ser usado para retornar um ServerSentEvents resultado. O primeiro parâmetro para esse método é um IAsyncEnumerable<SseItem<T>> que representa o fluxo de mensagens de evento a serem enviadas ao cliente.

O exemplo a seguir ilustra como usar a TypedResults.ServerSentEvents API para retornar um fluxo de eventos de freqüência cardíaca como objetos JSON para o cliente:

app.MapGet("/json-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<HeartRateRecord> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return HeartRateRecord.Create(heartRate);
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken),
                                                  eventType: "heartRate");
});

Para obter mais informações, consulte:

OpenAPI

Esta seção descreve novos recursos para OpenAPI.

Suporte ao OpenAPI 3.1

ASP.NET Core adicionou suporte para gerar documentos OpenAPI versão 3.1 no .NET 10. Apesar do incremento de versão menor, o OpenAPI 3.1 é uma atualização significativa para a especificação OpenAPI, em particular com suporte completo para o rascunho do JSON Schema 2020-12.

Algumas das alterações que você verá no documento OpenAPI gerado incluem:

  • Tipos anuláveis não têm mais a propriedade nullable: true no esquema.
  • Em vez de uma propriedade nullable: true, eles têm uma palavra-chave type cujo valor é uma matriz que inclui null como um dos tipos.
  • Propriedades ou parâmetros definidos como um C# int ou long agora aparecem no documento OpenAPI gerado sem o type: integer campo e têm um pattern campo que limita o valor a dígitos. Isso acontece quando a propriedade NumberHandling no JsonSerializerOptions é configurada para AllowReadingFromString, o padrão para aplicativos Web do ASP.NET Core. Para permitir que C# int e long sejam representados no documento OpenAPI como type: integer, defina a propriedade NumberHandling como long.

Com esse recurso, a versão padrão do OpenAPI para documentos gerados é3.1. A versão pode ser alterada definindo explicitamente a propriedade OpenApiVersion do OpenApiOptions no configureOptions parâmetro delegado de AddOpenApi:

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});

Ao gerar o documento OpenAPI no momento da compilação, a versão do OpenAPI pode ser selecionada definindo o item --openapi-version no OpenApiGenerateDocumentsOptions do MSBuild.

<PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
    <!-- Configure build-time OpenAPI generation to produce an OpenAPI 3.1 document. -->
    <OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

O suporte ao OpenAPI 3.1 foi adicionado principalmente na PR a seguir.

Alterações significativas no OpenAPI 3.1

O suporte para OpenAPI 3.1 requer uma atualização para a biblioteca de OpenAPI.NET subjacente para uma nova versão principal, 2.0. Esta nova versão tem algumas alterações significativas da versão anterior. As alterações significativas podem afetar aplicativos se eles tiverem quaisquer transformadores de documento, operação ou esquema. Alterações significativas nesta iteração incluem o seguinte:

  • Entidades dentro do documento OpenAPI, como operações e parâmetros, são digitadas como interfaces. Existem implementações concretas para as variantes inseridas e referenciadas de uma entidade. Por exemplo, um IOpenApiSchema pode ser um OpenApiSchema embutido ou um OpenApiSchemaReference que aponta para um esquema definido em outro lugar no documento.
  • A propriedade Nullable foi removida do tipo OpenApiSchema. Para determinar se um tipo é anulável, avalie se a propriedade OpenApiSchema.Type define JsonSchemaType.Null.

Uma das mudanças mais significativas é que a classe OpenApiAny foi descartada em favor do uso JsonNode diretamente. Os transformadores que usam OpenApiAny precisam ser atualizados para usar JsonNode. A seguinte diferença mostra as alterações no transformador de esquema do .NET 9 para o .NET 10:

options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
    if (context.JsonTypeInfo.Type == typeof(WeatherForecast))
    {
-       schema.Example = new OpenApiObject
+       schema.Example = new JsonObject
        {
-           ["date"] = new OpenApiString(DateTime.Now.AddDays(1).ToString("yyyy-MM-dd")),
+           ["date"] = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd"),
-           ["temperatureC"] = new OpenApiInteger(0),
+           ["temperatureC"] = 0,
-           ["temperatureF"] = new OpenApiInteger(32),
+           ["temperatureF"] = 32,
-           ["summary"] = new OpenApiString("Bracing"),
+           ["summary"] = "Bracing",
        };
    }
    return Task.CompletedTask;
});

Observe que essas alterações são necessárias mesmo ao configurar apenas a versão do OpenAPI para 3.0.

OpenAPI em YAML

ASP.NET agora dá suporte ao serviço do documento OpenAPI gerado no formato YAML. YAML pode ser mais conciso do que JSON, eliminando chaves curvas e aspas quando essas podem ser inferidas. O YAML também dá suporte a cadeias de caracteres de várias linhas, que podem ser úteis para descrições longas.

Para configurar um aplicativo para atender ao documento OpenAPI gerado no formato YAML, especifique o ponto de extremidade na chamada MapOpenApi com um sufixo ".yaml" ou ".yml", conforme mostrado no exemplo a seguir:

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi("/openapi/{documentName}.yaml");
}

Suporte para:

  • Atualmente, o YAML só está disponível para o OpenAPI servido a partir do endpoint OpenAPI.
  • A geração de documentos OpenAPI no formato YAML no momento da compilação é adicionada em uma versão prévia futura.

Consulte este PR, que adicionou suporte para disponibilizar o documento OpenAPI gerado em formato YAML.

Descrição da resposta em ProducesResponseType para controladores de API

Os atributos ProducesAttribute, ProducesResponseTypeAttribute e ProducesDefaultResponseType agora aceitam um parâmetro de cadeia de caracteres opcional, Descriptionque definirá a descrição da resposta. Veja um exemplo:

[HttpGet(Name = "GetWeatherForecast")]
[ProducesResponseType<IEnumerable<WeatherForecast>>(StatusCodes.Status200OK,
                   Description = "The weather forecast for the next 5 days.")]
public IEnumerable<WeatherForecast> Get()
{

E o OpenAPI gerado:

        "responses": {
          "200": {
            "description": "The weather forecast for the next 5 days.",
            "content": {

Atualmente, APIs mínimas não têm suporteProducesResponseType.

Contribuição da comunidade por Sander ten Brinke🙏

Preencher comentários de documentos XML no documento OpenAPI

A geração de documentos OpenAPI de ASP.NET Core agora incluirá metadados de comentários de documentação XML nas definições de métodos, classes e membros no documento OpenAPI. Você deve habilitar os comentários do documento XML em seu arquivo de projeto para usar esse recurso. Você pode fazer isso adicionando a seguinte propriedade ao arquivo de projeto:

  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

No tempo de compilação, o pacote OpenAPI utilizará um gerador de código-fonte para descobrir comentários XML no assembly da aplicação atual e quaisquer referências de projeto, emitindo código-fonte para inseri-los no documento por meio de um transformador de documento OpenAPI.

Observe que o processo de build do C# não captura comentários de documento XML colocados em expresões lambda, portanto, para usar comentários de documento XML para adicionar metadados a um ponto de extremidade de API mínimo, você deve definir o manipulador de ponto de extremidade como um método, colocar os comentários do documento XML sobre o método e, em seguida, referenciar esse método do método MapXXX. Por exemplo, para usar comentários de documento XML para adicionar metadados a um ponto de extremidade de API mínimo originalmente definido como uma expressão lambda:

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

Altere a chamada MapGet para fazer referência a um método:

app.MapGet("/hello", Hello);

Defina o método Hello com comentários de documento XML:

static partial class Program
{
    /// <summary>
    /// Sends a greeting.
    /// </summary>
    /// <remarks>
    /// Greeting a person by their name.
    /// </remarks>
    /// <param name="name">The name of the person to greet.</param>
    /// <returns>A greeting.</returns>
    public static string Hello(string name)
    {
        return $"Hello, {name}!";
    }
}

No exemplo anterior, o método Hello é adicionado à classe Program, mas você pode adicioná-lo a qualquer classe em seu projeto.

O exemplo anterior ilustra os comentários dos documentos XML <summary>, <remarks>e <param>. Para obter mais informações sobre comentários de documentos XML, incluindo todas as marcas com suporte, consulte a documentação do C#.

Como a funcionalidade principal é fornecida por meio de um gerador de origem, ela pode ser desabilitada adicionando o MSBuild a seguir ao arquivo de projeto.

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.2.*" GeneratePathProperty="true" />
</ItemGroup>

<Target Name="DisableCompileTimeOpenApiXmlGenerator" BeforeTargets="CoreCompile">
  <ItemGroup>
    <Analyzer Remove="$(PkgMicrosoft_AspNetCore_OpenApi)/analyzers/dotnet/cs/Microsoft.AspNetCore.OpenApi.SourceGenerators.dll" />
  </ItemGroup>
</Target>

O gerador de origem processa os arquivos XML incluídos na propriedade AdditionalFiles. Para adicionar (ou remover), as fontes modificam a propriedade da seguinte maneira:

<Target Name="AddXmlSources" BeforeTargets="CoreCompile">
  <ItemGroup>
    <AdditionalFiles Include="$(PkgSome_Package)/lib/net10.0/Some.Package.xml" />
  </ItemGroup>
</Target>

Microsoft.AspNetCore.OpenApi adicionado ao modelo de API Web do ASP.NET Core (AOT Nativo)

O modelo de projeto da API Web do ASP.NET Core (AOT Nativo) (nome webapiaotcurto) agora inclui suporte para a geração de documentos OpenAPI usando o Microsoft.AspNetCore.OpenApi pacote por padrão. Esse suporte é desabilitado usando o --no-openapi sinalizador ao criar um novo projeto.

Esta foi uma contribuição da comunidade por @sander1095. Obrigado por essa contribuição!

Suporte para IOpenApiDocumentProvider no contêiner de DI.

ASP.NET Core no .NET 10 dá suporte a IOpenApiDocumentProvider no contêiner de DI (injeção de dependência). Os desenvolvedores podem injetar IOpenApiDocumentProvider em seus aplicativos e usá-lo para acessar o documento OpenAPI. Essa abordagem é útil para acessar documentos OpenAPI fora do contexto de solicitações HTTP, como em serviços em segundo plano ou middleware personalizado.

Anteriormente, a execução da lógica de inicialização do aplicativo sem iniciar um servidor HTTP poderia ser feita usando HostFactoryResolver com uma implementação no-op IServer. O novo recurso simplifica esse processo fornecendo uma API simplificada inspirada na Aspire's IDistributedApplicationPublisher, que faz parte da estrutura da Aspire para hospedagem e publicação de aplicativos distribuídos.

Para obter mais informações, consulte dotnet/aspnetcore #61463.

Melhorias no gerador de comentários XML

A geração de comentários XML lida com tipos complexos no .NET 10 melhor do que as versões anteriores do .NET.

  • Ele produz comentários XML precisos e completos para uma gama mais ampla de tipos.
  • Ele lida com cenários mais complexos.
  • Ele contorna de forma elegante o processamento para tipos complexos que causam erros de compilação em versões anteriores.

Essas melhorias alteram o modo de falha para determinados cenários, desde erros de build até metadados ausentes.

Além disso, o processamento de comentários do documento XML agora pode ser configurado para acessar comentários XML em outros assemblies. Isso é útil para gerar documentação para tipos definidos fora do assembly atual, como o ProblemDetails tipo no Microsoft.AspNetCore.Http namespace.

Essa configuração é feita com diretivas no arquivo de build do projeto. O exemplo a seguir mostra como configurar o gerador de comentários XML para acessar comentários XML para tipos no assembly Microsoft.AspNetCore.Http, que inclui a classe ProblemDetails.

<Target Name="AddOpenApiDependencies" AfterTargets="ResolveReferences">
  <ItemGroup>
  <!-- Include XML documentation from Microsoft.AspNetCore.Http.Abstractions
    to get metadata for ProblemDetails -->
    <AdditionalFiles
          Include="@(ReferencePath->'
            %(RootDir)%(Directory)%(Filename).xml')"
          Condition="'%(ReferencePath.Filename)' ==
           'Microsoft.AspNetCore.Http.Abstractions'"
          KeepMetadata="Identity;HintPath" />
  </ItemGroup>
</Target>

Esperamos incluir comentários XML de um conjunto selecionado de assemblies na estrutura compartilhada em visualizações futuras, para evitar a necessidade dessa configuração na maioria dos casos.

Suporte para gerar OpenApiSchemas em transformadores

Os desenvolvedores agora podem gerar um esquema para um tipo C# usando a mesma lógica que ASP.NET geração de documentos OpenAPI Core e adicioná-lo ao documento OpenAPI. Em seguida, o esquema pode ser referenciado de outro lugar no documento OpenAPI.

O contexto passado para os transformadores de documento, operação e esquema inclui um novo GetOrCreateSchemaAsync método que pode ser usado para gerar um esquema para um tipo. Esse método também tem um parâmetro opcional ApiParameterDescription para especificar metadados adicionais para o esquema gerado.

Para dar suporte à adição do esquema ao documento OpenAPI, uma Document propriedade foi adicionada aos contextos de transformador de operação e esquema. Isso permite que qualquer transformador adicione um esquema ao documento OpenAPI usando o método do AddComponent documento.

Exemplo

Para usar esse recurso em um documento, operação ou transformador de esquema, crie o esquema usando o GetOrCreateSchemaAsync método fornecido no contexto e adicione-o ao documento OpenAPI usando o método AddComponent do documento.

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer(async (operation, context, cancellationToken) =>
    {
        // Generate schema for error responses
        var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), null, cancellationToken);
        context.Document?.AddComponent("Error", errorSchema);

        operation.Responses ??= new OpenApiResponses();
        // Add a "4XX" response to the operation with the newly created schema
        operation.Responses["4XX"] = new OpenApiResponse
        {
            Description = "Bad Request",
            Content = new Dictionary<string, OpenApiMediaType>
            {
                ["application/problem+json"] = new OpenApiMediaType
                {
                    Schema = new OpenApiSchemaReference("Error", context.Document)
                }
            }
        };
    });
});

OpenAPI.NET atualizado para Preview.18

A biblioteca de OpenAPI.NET usada na geração de documentos do ASP.NET Core OpenAPI foi atualizada para v2.0.0-preview18. A versão v2.0.0-preview18 melhora a compatibilidade com a versão atualizada da biblioteca.

A versão anterior v2.0.0-preview17 incluiu várias correções de bugs e melhorias e também introduziu algumas alterações que causam incompatibilidades. As mudanças disruptivas devem afetar apenas os usuários que usam transformadores de documento, operação ou esquema. Alterações significativas nesta versão que podem afetar os desenvolvedores incluem o seguinte:

Autenticação e autorização

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

Métricas de autenticação e autorização

As métricas foram adicionadas para determinados eventos de autenticação e autorização no ASP.NET Core. Com essa alteração, agora você pode obter métricas para os seguintes eventos:

  • Autenticação:
    • Duração da solicitação autenticada
    • Contagem de desafios
    • Contagem de proibições
    • Contagem de entradas
    • Contagem de desconexões
  • Autorização:
    • Contagem de solicitações que exigem autorização

A imagem a seguir mostra um exemplo da métrica de duração da solicitação autenticada no painel do Aspire:

Duração da solicitação autenticada no painel do Aspire

Para obter mais informações, consulte ASP.NET Principais métricas de Autorização e Autenticação.

Variado

Esta seção descreve novos recursos diversos no .NET 10.

Descritores de segurança personalizáveis para HTTP.sys

Agora você pode especificar um descritor de segurança personalizado para filas de solicitação HTTP.sys. A nova propriedade RequestQueueSecurityDescriptor permite HttpSysOptions um controle mais granular sobre os direitos de acesso da fila de solicitação. Esse controle granular permite que você adapte a segurança às necessidades do aplicativo.

O que você pode fazer com a nova propriedade

Uma fila de solicitações no HTTP.sys é uma estrutura no nível do kernel que armazena temporariamente solicitações HTTP de entrada até que seu aplicativo esteja pronto para processá-las. Ao personalizar o descritor de segurança, você pode permitir ou negar acesso de usuários ou grupos específicos à fila de solicitações. Isso é útil em cenários em que você deseja restringir ou delegar o tratamento de solicitações HTTP.sys no nível do sistema operacional.

Como usar a nova propriedade

A RequestQueueSecurityDescriptor propriedade se aplica somente ao criar uma nova fila de solicitação. A propriedade não afeta as filas de solicitação existentes. Para usar essa propriedade, defina-a como uma GenericSecurityDescriptor instância ao configurar o servidor HTTP.sys.

Por exemplo, o código a seguir permite todos os usuários autenticados, mas nega aos convidados:

using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.HttpSys;

// Create a new security descriptor
var securityDescriptor = new CommonSecurityDescriptor(isContainer: false, isDS: false, sddlForm: string.Empty);

// Create a discretionary access control list (DACL)
var dacl = new DiscretionaryAcl(isContainer: false, isDS: false, capacity: 2);
dacl.AddAccess(
    AccessControlType.Allow,
    new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
    -1,
    InheritanceFlags.None,
    PropagationFlags.None
);
dacl.AddAccess(
    AccessControlType.Deny,
    new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null),
    -1,
    InheritanceFlags.None,
    PropagationFlags.None
);

// Assign the DACL to the security descriptor
securityDescriptor.DiscretionaryAcl = dacl;

// Configure HTTP.sys options
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseHttpSys(options =>
{
    options.RequestQueueSecurityDescriptor = securityDescriptor;
});

Para obter mais informações, consulte HTTP.sys implementação do servidor Web no ASP.NET Core.

Melhor suporte para testar aplicativos com instruções de nível superior

O .NET 10 agora tem melhor suporte para testar aplicativos que usam instruções de nível superior. Anteriormente, os desenvolvedores tinham que adicionar manualmente public partial class Program ao arquivo de Program.cs para que o projeto de teste pudesse referenciar o Program class. public partial class Program era necessário porque o recurso de instrução de nível superior no C# 9 gerou um Program class que foi declarado como interno.

No .NET 10, um gerador de origem é usado para gerar a declaração de public partial class Program se o programador não a declarou explicitamente. Além disso, um analisador foi adicionado para detectar quando public partial class Program é declarado explicitamente e aconselha o desenvolvedor a removê-lo.

Imagem

Os seguintes PRs contribuíram para esta funcionalidade:

Nova implementação de Patch JSON com System.Text.Json

Patch JSON:

  • É um formato padrão para descrever as alterações a serem aplicadas a um documento JSON.
  • É definido no RFC 6902 e é amplamente usado em APIs RESTful para executar atualizações parciais para recursos JSON.
  • Representa uma sequência de operações (por exemplo, Adicionar, Remover, Substituir, Mover, Copiar, Testar) que podem ser aplicadas para modificar um documento JSON.

Em aplicativos Web, o Patch JSON geralmente é usado em uma operação PATCH para executar atualizações parciais de um recurso. Em vez de enviar todo o recurso para uma atualização, os clientes podem enviar um documento de patch JSON contendo apenas as alterações. A aplicação de patch reduz o tamanho da carga e melhora a eficiência.

Esta versão apresenta uma nova implementação de Microsoft.AspNetCore.JsonPatch baseada na serialização System.Text.Json. Esse recurso:

  • Alinha-se às práticas modernas do .NET aproveitando a System.Text.Json biblioteca, que é otimizada para .NET.
  • Fornece desempenho aprimorado e consumo de memória reduzido em comparação com a implementação baseada em legado Newtonsoft.Json.

Os parâmetros de comparação a seguir comparam o desempenho da nova System.Text.Json implementação com a implementação herdada Newtonsoft.Json .

Cenário Implementação Média Memória alocada
Parâmetros de comparação de aplicativo Newtonsoft.JsonPatch 271,924 μs 25 KB
System.Text.JsonPatch 1,584 μs 3 KB
Parâmetros de comparação de desserialização Newtonsoft.JsonPatch 19,261 μs 43 KB
System.Text.JsonPatch 7,917 μs 7 KB

Esses parâmetros de comparação destacam ganhos significativos de desempenho e redução do uso de memória com a nova implementação.

Observações:

  • A nova implementação não é uma substituição direta para a implementação legada. Em particular, a nova implementação não dá suporte a tipos dinâmicos, por exemplo, ExpandoObject.
  • O padrão de patch JSON tem riscos de segurança inerentes. Como esses riscos são inerentes ao padrão de Patch JSON, a nova implementação não tenta reduzir os riscos inerentes à segurança. É responsabilidade do desenvolvedor garantir que o documento do Patch JSON seja seguro para aplicar ao objeto de destino. Para obter mais informações, consulte a seção Mitigando riscos de segurança .

Uso

Para habilitar o suporte a JSON Patch com System.Text.Json, instale o pacote NuGet Microsoft.AspNetCore.JsonPatch.SystemTextJson.

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease

Este pacote fornece uma JsonPatchDocument<T> classe para representar um documento de Patch JSON para objetos de tipo T e lógica personalizada para serializar e desserializar documentos de Patch JSON usando System.Text.Json. O método chave da JsonPatchDocument<T> classe é ApplyTo, que aplica as operações de patch a um objeto de destino do tipo T.

Os exemplos a seguir demonstram como usar o ApplyTo método para aplicar um documento de Patch JSON a um objeto.

Exemplo: aplicação de um JsonPatchDocument

O exemplo a seguir demonstra:

  1. As operações add, replace e remove.
  2. Operações em propriedades aninhadas.
  3. Adicionando um novo item a uma matriz.
  4. Usando um conversor de enumeração de cadeia de caracteres JSON em um documento de Patch JSON.
// Original object
var person = new Person {
  FirstName = "John",
  LastName = "Doe",
  Email = "johndoe@gmail.com",
  PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
  Address = new Address
  {
    Street = "123 Main St",
    City = "Anytown",
    State = "TX"
  }
};

// Raw JSON Patch document
var jsonPatch = """
[
  { "op": "replace", "path": "/FirstName", "value": "Jane" },
  { "op": "remove", "path": "/Email"},
  { "op": "add", "path": "/Address/ZipCode", "value": "90210" },
  {
    "op": "add",
    "path": "/PhoneNumbers/-",
    "value": { "Number": "987-654-3210", "Type": "Work" }
  }
]
""";

// Deserialize the JSON Patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON Patch document
patchDoc!.ApplyTo(person);

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));

// Output:
// {
//   "firstName": "Jane",
//   "lastName": "Doe",
//   "address": {
//     "street": "123 Main St",
//     "city": "Anytown",
//     "state": "TX",
//     "zipCode": "90210"
//   },
//   "phoneNumbers": [
//     {
//       "number": "123-456-7890",
//       "type": "Mobile"
//     },
//     {
//       "number": "987-654-3210",
//       "type": "Work"
//     }
//   ]
// }

O ApplyTo método geralmente segue as convenções e opções de System.Text.Json para processamento do JsonPatchDocument, incluindo o comportamento controlado pelas seguintes opções:

  • NumberHandling: se as propriedades numéricas são lidas de cadeias de caracteres.
  • PropertyNameCaseInsensitive: se os nomes de propriedade são sensíveis a maiúsculas e minúsculas.

Principais diferenças entre System.Text.Json e a nova JsonPatchDocument<T> implementação:

  • O tipo de tempo de execução do objeto de destino, não o tipo declarado, determina quais propriedades ApplyTo altera.
  • System.Text.Json A desserialização depende do tipo declarado para identificar as propriedades elegíveis.

Exemplo: aplicação de um JsonPatchDocument com tratamento de erros

Há vários erros que podem ocorrer ao aplicar um documento de patch JSON. Por exemplo, o objeto de destino pode não ter a propriedade especificada ou o valor especificado pode ser incompatível com o tipo de propriedade.

O Patch JSON também dá suporte à operação test. A test operação verifica se um valor especificado é igual à propriedade de destino e, se não, retorna um erro.

O exemplo a seguir demonstra como lidar com esses erros normalmente.

Importante

O objeto passado para o ApplyTo método é modificado no local. É responsabilidade do chamador descartar essas alterações se alguma operação falhar.

// Original object
var person = new Person {
  FirstName = "John",
  LastName = "Doe",
  Email = "johndoe@gmail.com"
};

// Raw JSON Patch document
var jsonPatch = """
[
  { "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
  { "op": "test", "path": "/FirstName", "value": "Jane" },
  { "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";

// Deserialize the JSON Patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON Patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
    {
        errors ??= new ();
        var key = jsonPatchError.AffectedObject.GetType().Name;
        if (!errors.ContainsKey(key))
        {
            errors.Add(key, new string[] { });
        }
        errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
    });
if (errors != null)
{
    // Print the errors
    foreach (var error in errors)
    {
        Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
    }
}

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));

// Output:
// Error in Person: The current value 'John' at path 'FirstName' is not equal 
// to the test value 'Jane'.
// {
//   "firstName": "John",
//   "lastName": "Smith",              <<< Modified!
//   "email": "janedoe@gmail.com",     <<< Modified!
//   "phoneNumbers": []
// }

Mitigação dos riscos à segurança

Ao usar o Microsoft.AspNetCore.JsonPatch.SystemTextJson pacote, é essencial entender e atenuar possíveis riscos de segurança. As seções a seguir descrevem os riscos de segurança identificados associados ao Patch JSON e fornecem mitigações recomendadas para garantir o uso seguro do pacote.

Importante

Essa não é uma lista completa de ameaças. Os desenvolvedores de aplicativos devem realizar suas próprias revisões de modelo de ameaças para determinar uma lista abrangente específica do aplicativo e criar mitigações apropriadas, conforme necessário. Por exemplo, os aplicativos que expõem coleções a operações de patch devem considerar o potencial de ataques de complexidade algorítmica se essas operações inserirem ou removerem elementos no início da coleção.

Ao executar modelos de ameaças abrangentes para seus próprios aplicativos e lidar com ameaças identificadas, seguindo as mitigações recomendadas abaixo, os consumidores desses pacotes podem integrar a funcionalidade do Patch JSON em seus aplicativos, minimizando os riscos de segurança.

Os consumidores desses pacotes podem integrar a funcionalidade do Patch JSON em seus aplicativos, minimizando os riscos de segurança, incluindo:

  • Execute modelos de ameaças abrangentes para seus próprios aplicativos.
  • Abordar ameaças identificadas.
  • Siga as mitigações recomendadas nas seções a seguir.
Negação de Serviço (DoS) por meio da amplificação de memória
  • Cenário: um cliente mal-intencionado envia uma copy operação que duplica grafos de objetos grandes várias vezes, levando ao consumo excessivo de memória.
  • Impacto: Possíveis condições de out-Of-Memory (OOM), causando interrupções no serviço.
  • Mitigação:
    • Valide os documentos JSON Patch de entrada quanto ao tamanho e à estrutura antes de chamar ApplyTo.
    • A validação deve ser específica do aplicativo, mas uma validação de exemplo pode ser semelhante à seguinte:
public void Validate(JsonPatchDocument<T> patch)
{
    // This is just an example. It's up to the developer to make sure that
    // this case is handled properly, based on the app's requirements.
    if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
        > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}
Subversão lógica de negócios
  • Cenário: as operações de patch podem manipular campos com invariáveis implícitas, (por exemplo, sinalizadores internos, IDs ou campos computados), violando restrições comerciais.
  • Impacto: problemas de integridade de dados e comportamento de aplicativo não intencional.
  • Mitigação:
    • Use objetos POCO com propriedades definidas explicitamente que são seguras de modificar.
    • Evite expor propriedades confidenciais ou críticas à segurança no objeto de destino.
    • Se nenhum objeto POCO for usado, valide o objeto corrigido após a aplicação de operações para garantir que regras de negócios e invariáveis não sejam violados.
Autenticação e autorização
  • Cenário: clientes não autenticados ou não autorizados enviam solicitações de patch JSON mal-intencionadas.
  • Impacto: acesso não autorizado para modificar dados confidenciais ou interromper o comportamento do aplicativo.
  • Mitigação:
    • Proteja os endpoints que aceitam solicitações JSON Patch com mecanismos de autenticação e autorização adequados.
    • Restrinja o acesso a clientes confiáveis ou usuários com permissões apropriadas.

Detectar se a URL é local usando RedirectHttpResult.IsLocalUrl

Use o novo método auxiliar RedirectHttpResult.IsLocalUrl(url) para detectar se uma URL é local. Uma URL será considerada local se o seguinte for verdadeiro:

URLs que usam caminhos virtuais"~/" também são locais.

IsLocalUrl é útil para validar URLs antes de redirecioná-las para evitar ataques de redirecionamento abertos.

if (RedirectHttpResult.IsLocalUrl(url))
{
    return Results.LocalRedirect(url);
}

Obrigado @martincostello por esta contribuição!

HTTP.sys implementação do servidor Web no ASP.NET Core