Compartir a través de


Novedades de ASP.NET Core 9.0

En este artículo se resaltan los cambios más importantes de ASP.NET Core 9.0, con vínculos a la documentación pertinente.

Este artículo se ha actualizado para .NET 9 (versión preliminar 7).

Blazor

En esta sección se describen las nuevas características de Blazor.

.NET MAUIBlazor Hybrid y plantilla de solución de aplicación web

Una nueva plantilla de solución facilita la creación de aplicaciones .NET MAUI nativas y Blazor cliente web que comparten la misma interfaz de usuario. Esta plantilla muestra cómo crear aplicaciones cliente que maximicen la reutilización del código y tengan como destino Android, iOS, Mac, Windows y la Web.

Entre las características clave de esta plantilla se incluyen las siguientes:

  • La capacidad de elegir un modo de representación interactivo de Blazor para la aplicación web.
  • Creación automática de los proyectos adecuados, incluyendo una Blazor Web App (representación automática interactiva global) y una aplicación .NET MAUIBlazor Hybrid.
  • Los proyectos creados usan una biblioteca de clases compartidas (RCL) Razor para mantener los componentes de Razor de la interfaz de usuario.
  • Se incluye código de ejemplo que demuestra cómo usar la inyección de dependencias para proporcionar diferentes implementaciones de interfaces para la aplicación Blazor Hybrid y la Blazor Web App.

Para empezar, instala el SDK de .NET 9 y la carga de trabajo .NET MAUI, que contiene la plantilla:

dotnet workload install maui

Crea una solución a partir de la plantilla de proyecto en un shell de comandos mediante el comando siguiente:

dotnet new maui-blazor-web

La plantilla también está disponible en Visual Studio.

Nota:

Actualmente, se produce una excepción si se definen modos de representación de Blazor a nivel de página/componente. Para obtener más información, consulta BlazorWebView necesita una manera de habilitar la invalidación de ResolveComponentForRenderMode (dotnet/aspnetcore #51235).

Para más información, consulta Compilar una aplicación .NET MAUIBlazor Hybrid con una Blazor Web App.

Optimización de entrega de recursos estáticos

MapStaticAssets es un nuevo middleware que ayuda a optimizar la entrega de recursos estáticos en cualquier aplicación ASP.NET Core, incluidas las aplicaciones de Blazor.

Para más información, consulta cualquiera de los siguientes recursos:

Detección de la ubicación de representación, interactividad y modo de representación asignado en tiempo de ejecución

Hemos introducido una nueva API diseñada para simplificar el proceso de consulta de estados de componentes en tiempo de ejecución. Esta API proporciona las siguientes funcionalidades:

  • Determinar la ubicación de ejecución actual del componente: esto puede ser especialmente útil para depurar y optimizar el rendimiento de los componentes.
  • Comprobar si el componente se ejecuta en un entorno interactivo: esto puede resultar útil para los componentes que tienen comportamientos diferentes en función de la interactividad de su entorno.
  • Recuperar el modo de representación asignado para el componente: comprender el modo de representación puede ayudar a optimizar el proceso de representación y mejorar el rendimiento general de un componente.

Para obtener más información, consulta Modos de representación de Blazor de ASP.NET Core.

Experiencia mejorada de reconexión del lado servidor:

Se han realizado las siguientes mejoras en la experiencia de reconexión predeterminada del lado servidor:

  • Cuando el usuario vuelve a una aplicación con un circuito desconectado, se intenta volver a conectar inmediatamente en lugar de esperar la duración del siguiente intervalo de reconexión. Esto mejora la experiencia del usuario cuando navega a una aplicación en una pestaña del explorador que ha pasado a estado de suspensión.

  • Cuando un intento de reconexión llega al servidor, pero el servidor ya ha liberado el circuito, se produce automáticamente una actualización de página. Esto evita que el usuario tenga que actualizar manualmente la página si es probable que se produzca una reconexión correcta.

  • El tiempo de reconexión usa una estrategia de retroceso calculada. De forma predeterminada, los primeros intentos de reconexión se producen en sucesión rápida sin un intervalo de reintento antes de que se introduzcan retrasos calculados entre intentos. Puedes personalizar el comportamiento del intervalo de reintento especificando una función para calcular el intervalo de reintento, como se muestra en el ejemplo de retroceso exponencial siguiente:

    Blazor.start({
      circuit: {
        reconnectionOptions: {
          retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
            previousAttempts >= maxRetries ? null : previousAttempts * 1000
        },
      },
    });
    
  • Se ha modernizado el estilo de la interfaz de usuario de reconexión predeterminada.

Para obtener más información, consulta las Instrucciones de ASP.NET Core BlazorSignalR.

Serialización simplificada del estado de autenticación para Blazor Web App

Las nuevas API facilitan la adición de autenticación a una Blazor Web App existente. Al crear una Blazor Web App con autenticación mediante cuentas individuales y habilitar la interactividad basada en WebAssembly, el proyecto incluye un AuthenticationStateProvider personalizado en los proyectos de servidor y cliente.

Estos proveedores hacen que el estado de autenticación del usuario fluya al explorador. La autenticación en el servidor en lugar del cliente permite que la aplicación acceda al estado de autenticación durante la representación previa y antes de que se inicialice el runtime de .NET WebAssembly.

Las implementaciones personalizadas de AuthenticationStateProvider usan el servicio de estado de componente persistente (PersistentComponentState) para serializar el estado de autenticación en comentarios HTML y, después, volverlo a leer desde WebAssembly para crear una instancia de AuthenticationState.

Esto funciona bien si comenzaste a partir de la plantilla de proyecto de Blazor Web App y seleccionaste la opción Cuentas individuales, pero es mucho código que implementar personalmente o para copiarlo si lo que intentas es agregar autenticación a un proyecto existente. Ahora existen API, que forman parte de la plantilla de proyecto de Blazor Web App, que pueden llamarse en los proyectos del servidor y del cliente para agregar esta funcionalidad:

  • AddAuthenticationStateSerialization: agrega los servicios necesarios para serializar el estado de autenticación en el servidor.
  • AddAuthenticationStateDeserialization: agrega los servicios necesarios para deserializar el estado de autenticación en el explorador.

De manera predeterminada, estas API solo serializan las notificaciones de rol y nombre del lado servidor para el acceso en el explorador. Se puede pasar una opción a AddAuthenticationStateSerialization para incluir todas las notificaciones.

Para obtener más información, consulta las secciones siguientes de Protección de aplicaciones Blazor del lado servidor de ASP.NET Core:

Agregar páginas estáticas de representación del lado del servidor (SSR) a una Blazor Web App interactiva de manera global

Con el lanzamiento de .NET 9, ahora es más sencillo agregar páginas de SSR estática a las aplicaciones que adoptan interactividad global.

Este enfoque solo es útil cuando la aplicación tiene páginas específicas que no pueden trabajar con la representación interactiva de Servidor o WebAssembly. Por ejemplo, adopta este enfoque para las páginas que dependen de la lectura y escritura de cookies de HTTP y solo pueden funcionar en un ciclo de solicitud/respuesta en lugar de la representación interactiva. En el caso de las páginas que funcionan con la representación interactiva, no debes obligarlas a usar la representación SSR estática, ya que es menos eficaz y tiene menos capacidad de respuesta para el usuario final.

Marca cualquier página de componentes Razor con el nuevo atributo [ExcludeFromInteractiveRouting] asignado con la directiva Razor@attribute:

@attribute [ExcludeFromInteractiveRouting]

La aplicación del atributo hace que la navegación a la página salga del enrutamiento interactivo. La navegación entrante se ve forzada a realizar una recarga de página completa en su lugar resolviendo la página a través del enrutamiento interactivo. La recarga de página completa obliga al componente raíz de nivel superior, normalmente el componente App (App.razor), a volver a representar desde el servidor, lo que permite que la aplicación cambie a otro modo de representación de nivel superior.

El método de extensión HttpContext.AcceptsInteractiveRouting permite al componente detectar si [ExcludeFromInteractiveRouting] se aplica a la página actual.

En el componente App, usa el patrón en el ejemplo siguiente:

  • Páginas que no están anotadas con el valor predeterminado [ExcludeFromInteractiveRouting] para el modo de representación InteractiveServer con interactividad global. Puedes reemplazar InteractiveServer por InteractiveWebAssembly o InteractiveAuto para especificar otro modo de representación global predeterminado.
  • Páginas anotadas con la adopción de SSR estática [ExcludeFromInteractiveRouting] (a PageRenderMode se le asigna 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;
}

Una alternativa al uso del método de extensión HttpContext.AcceptsInteractiveRouting es leer manualmente los metadatos del punto de conexión mediante HttpContext.GetEndpoint()?.Metadata.

Esta característica está cubierta por la documentación de referencia en los modelos de representación de Blazor de ASP.NET Core.

Inserción de constructores

Razor componentes admiten la inserción de constructores.

En el ejemplo siguiente, la clase parcial (código subyacente) inserta el servicio NavigationManager mediante un constructor principal:

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

Para obtener más información, consulta Inserción de dependencias de Blazor de ASP.NET Core.

Compresión de Websocket para componentes de Servidor interactivo

De forma predeterminada, los componentes de Interactive Server habilitan la compresión para las conexiones de WebSocket y establecen una directiva de seguridad de contenido (CSP) frame-ancestors establecida en 'self', que solo permite insertar la aplicación en un <iframe> origen desde el que se sirve la aplicación cuando se habilita la compresión o cuando se proporciona una configuración para el contexto de WebSocket.

La compresión se puede deshabilitar estableciendo ConfigureWebSocketOptions en null, lo que reduce la vulnerabilidad de la aplicación para atacar, pero puede dar lugar a una reducción del rendimiento:

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

Configura un frame-ancestorsCSP más estricto con un valor de 'none' (comillas simples necesarias), lo que permite la compresión de WebSocket, pero impide que los exploradores inserte la aplicación en cualquier <iframe>:

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

Para obtener más información, consulta los siguientes recursos:

Controlar eventos de composición de teclado en Blazor

La nueva propiedad KeyboardEventArgs.IsComposing indica si el evento de teclado forma parte de una sesión de composición. El seguimiento del estado de composición de los eventos de teclado es fundamental para controlar los métodos internacionales de entrada de caracteres.

Se ha agregado el parámetro OverscanCount a QuickGrid

El componente QuickGrid expone ahora una propiedad OverscanCount que especifica cuántas filas adicionales se representan antes y después de la región visible cuando la virtualización está habilitada.

El valor predeterminado de OverscanCount es 3. En el ejemplo siguiente, se aumenta OverscanCount a 4:

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

El componente InputNumber admite el atributo type="range"

El componente InputNumber<TValue> ahora admite el atributo type="range", que crea una entrada de rango que admite el enlace de modelos y la validación de formularios, que normalmente se representan como control deslizante o de marcado en lugar de un cuadro 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; }
    }
}

Varias Blazor Web App por proyecto de servidor

La compatibilidad con varias Blazor Web App por proyecto de servidor se considerará para .NET 10 (noviembre de 2025).

Para obtener más información, consulta Compatibilidad con varias aplicaciones web Blazor por proyecto de servidor (dotnet/aspnetcore #52216).

SignalR

En esta sección se describen las nuevas características de SignalR.

Compatibilidad con tipos polimórficos en concentradores SignalR

Los métodos de concentrador ahora pueden aceptar una clase base en lugar de la clase derivada para habilitar escenarios polimórficos. El tipo base debe estar anotado para permitir el 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; }
}

Actividades mejoradas para SignalR

SignalR ahora tiene un objeto ActivitySource denominado Microsoft.AspNetCore.SignalR.Server que emite eventos para las llamadas al método del centro:

  • Cada método es su propia actividad, por lo que todo lo que emita una actividad durante la llamada al método del centro se encuentra bajo la actividad del método del centro.
  • Las actividades del método del centro no tienen un elemento primario. Esto significa que no se agrupan en la conexión SignalR de ejecución prolongada.

En el siguiente ejemplo se usa el panel de .NET Aspire y los paquetes de 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" />

Agrega el siguiente código de inicio al archivo 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());

A continuación se muestra una salida de ejemplo del panel Aspire:

Lista de actividades para eventos de llamada al método de SignalR del centro

SignalR es compatible con recortes y AOT nativo

Continuando con el recorrido de AOT nativo iniciado en .NET 8, hemos habilitado el recorte y la compatibilidad nativa con la compilación anticipada (AOT) tanto para escenarios de cliente como de servidor SignalR. Ahora puedes aprovechar las ventajas de rendimiento de usar AOT nativo en aplicaciones que usan SignalR para las comunicaciones web en tiempo real.

Introducción

Instala el SDK de .NET 9 más reciente.

Crea una solución a partir de la plantilla webapiaot en un shell de comandos mediante el siguiente comando:

dotnet new webapiaot -o SignalRChatAOTExample

Reemplaza el contenido del archivo Program.cs por el siguiente código SignalR:

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);
    }
}

En el ejemplo anterior se genera un ejecutable nativo de Windows de 10 MB y un ejecutable de Linux de 10,9 MB.

Limitaciones

  • Actualmente solo se admite el protocolo JSON:
    • Como se muestra en el código anterior, las aplicaciones que usan la serialización JSON y AOT nativo deben usar el generador de orígenes System.Text.Json.
    • Esto sigue el mismo enfoque que las API mínimas.
  • En el servidor SignalR, no se admiten los parámetros de método de centro de conectividad de tipo IAsyncEnumerable<T> y ChannelReader<T> donde T es un ValueType (struct). El uso de estos tipos da como resultado una excepción en tiempo de ejecución al iniciarse en el desarrollo y en la aplicación publicada. Para obtener más información, consultaSignalR: Uso de IAsyncEnumerable<T> y ChannelReader<T> con ValueTypes en AOT nativo (dotnet/aspnetcore #56179).
  • Los centros fuertemente tipados no son compatibles con AOT nativo (PublishAot). El uso de concentradores fuertemente tipados con AOT nativo producirá advertencias durante la compilación y publicación, y una excepción en tiempo de ejecución. El uso de centros fuertemente tipados con recorte (PublishedTrimmed) es compatible.
  • Solo son compatibles Task, Task<T>, ValueTask o ValueTask<T> para los tipos de devolución asíncronos.

API mínimas

En esta sección se describen las nuevas características de las API mínimas.

Se ha agregado InternalServerError y InternalServerError<TValue> a TypedResults.

La clase TypedResults es un vehículo útil para devolver respuestas basadas en códigos de estado HTTP fuertemente tipados desde una API mínima. TypedResults ahora incluye métodos y tipos de fábrica para devolver las respuestas "Error interno del servidor 500" de los puntos de conexión. Este es un ejemplo que devuelve una respuesta 500:

var app = WebApplication.Create();

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

app.Run();

Llamada a ProducesProblem y ProducesValidationProblem en grupos de rutas

Los métodos de extensión ProducesProblem y ProducesValidationProblem se han actualizado para ser compatibles con su uso en grupos de rutas. Estos métodos indican que todos los puntos finales de un grupo de rutas pueden devolver respuestas ProblemDetails o ValidationProblemDetails para los metadatos 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

En esta sección se describen las nuevas características de OpenAPI

Compatibilidad integrada con la generación de documentos de OpenAPI

La especificación OpenAPI es un estándar para describir las API HTTP. El estándar permite a los desarrolladores definir la forma de las API que se pueden conectar a generadores de cliente, generadores de servidores, herramientas de prueba, documentación, etc. En la versión preliminar de .NET 9, ASP.NET Core proporciona compatibilidad integrada para generar documentos de OpenAPI que representan API mínimas o basadas en controladores mediante el paquete Microsoft.AspNetCore.OpenApi.

El código resaltado siguiente llama a:

  • AddOpenApi para registrar las dependencias necesarias en el contenedor de inserción de dependencias de la aplicación.
  • MapOpenApi para registrar los puntos de conexión de OpenAPI necesarios en las rutas de la aplicación.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

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

app.Run();

Instala el paquete Microsoft.AspNetCore.OpenApi en el proyecto mediante el siguiente comando:

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

Ejecuta la aplicación y ve a openapi/v1.json para ver el documento de OpenAPI generado:

Documento de OpenAPI

Los documentos de OpenAPI también se pueden generar en tiempo de compilación al agregar el paquete Microsoft.Extensions.ApiDescription.Server:

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

En el archivo de proyecto de la aplicación, agregue lo siguiente:

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

Ejecuta dotnet build e inspecciona el archivo JSON generado en el directorio del proyecto.

Generación de documentos de OpenAPI en tiempo de compilación

La generación de documentos OpenAPI integrada en ASP.NET Core admite varias personalizaciones y opciones. Proporciona transformadores de documentos y operaciones y tiene la capacidad de administrar varios documentos OpenAPI para la misma aplicación.

Para obtener más información sobre las nuevas funcionalidades del documento de OpenAPI de ASP.NET Core, consulta la nueva documentación de Microsoft.AspNetCore.OpenApi.

Mejoras de finalización de IntelliSense para el paquete OpenAPI

La compatibilidad con OpenAPI de ASP.NET Core ahora es más accesible y fácil de usar. Las API de OpenAPI se envían como un paquete independiente, separado del marco compartido. Hasta ahora, esto significaba que los desarrolladores no contaban con la comodidad de las características de finalización de código, como Intellisense para las API de OpenAPI.

Al reconocer esta carencia, hemos introducido un nuevo proveedor de finalización y un solucionador de código. Estas herramientas están diseñadas para facilitar la detección y el uso de las API de OpenAPI, lo que facilita a los desarrolladores la integración de OpenAPI en sus proyectos. El proveedor de finalización ofrece sugerencias de código en tiempo real, mientras que el solucionador de código ayuda a corregir errores comunes y a mejorar el uso de la API. Esta mejora forma parte de nuestro compromiso continuo para mejorar la experiencia del desarrollador y simplificar los flujos de trabajo relacionados con la API.

Cuando un usuario escribe una instrucción en la que está disponible una API relacionada con OpenAPI, el proveedor de finalización muestra una recomendación para la API. Por ejemplo, en las siguientes capturas de pantalla, las finalizaciones de AddOpenApi y MapOpenApi se proporcionan cuando un usuario escribe una instrucción de invocación en un tipo admitido, como IEndpointConventionBuilder:

Finalizaciones de OpenAPI

Cuando se acepta la finalización y no se instala el paquete Microsoft.AspNetCore.OpenApi, un codefixer proporciona un acceso directo para instalar automáticamente la dependencia en el proyecto.

Instalación automática de paquetes

Compatibilidad con atributos [Required] y [DefaultValue] en parámetros y propiedades

Cuando se aplican los atributos [Required] y [DefaultValue] en parámetros o propiedades dentro de tipos complejos, la implementación de OpenAPI asigna estas propiedades a las propiedades required y default del documento de OpenAPI asociado al esquema de tipo o parámetro.

Por ejemplo, la siguiente API genera el esquema complementario para el tipo 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"
	  }
	}
}

Compatibilidad con transformadores de esquema en documentos de OpenAPI

La compatibilidad integrada con OpenAPI ahora incluye compatibilidad con transformadores de esquema que se pueden usar para modificar esquemas generados por System.Text.Json y la implementación de OpenAPI. Al igual que los transformadores de documentos y operaciones, los transformadores de esquema se pueden registrar en el objeto OpenApiOptions. Por ejemplo, el siguiente ejemplo de código muestra el uso de un transformador de esquema para agregar un ejemplo al esquema de un 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; }
}

Mejoras en las API de registro de transformadores en Microsoft.AspNetCore.OpenApi

Los transformadores OpenAPI son compatibles con la modificación del documento de OpenAPI, las operaciones dentro del documento o los esquemas asociados a los tipos de la API. Las API para registrar transformadores en un documento de OpenAPI proporcionan una variedad de opciones para registrar transformadores.

Anteriormente, las siguientes API estaban disponibles 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>)

El nuevo conjunto de API es el siguiente:

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 es compatible con recortes y AOT nativo

La nueva compatibilidad integrada con OpenAPI en ASP.NET Core ahora también admite recortes y AOT nativo.

Introducción

Crea un nuevo proyecto de API web ASP.NET Core (AOT nativo).

dotnet new webapiaot

Agrega el paquete Microsoft.AspNetCore.OpenAPI.

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

Para esta versión preliminar, también debes agregar el paquete Microsoft.OpenAPI más reciente para evitar advertencias de recorte.

dotnet add package Microsoft.OpenApi

Actualiza Program.cs para habilitar la generación de documentos openAPI.

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

Publica la aplicación.

dotnet publish

La aplicación publica con AOT nativo sin advertencias.

Compatible con llamadas a ProducesProblem y ProducesValidationProblem en grupos de rutas

Se han actualizado los métodos de extensión ProducesProblem y ProducesValidationProblem para los grupos de rutas. Estos métodos se pueden usar para indicar que todos los puntos de conexión de un grupo de rutas pueden devolver respuestas ProblemDetails o ValidationProblemDetails para los metadatos 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);

Los tipos de resultado Problem y ValidationProblem son compatibles con la construcción con valores IEnumerable<KeyValuePair<string, object?>>

Antes de .NET 9, la construcción de los tipos de resultado Problem y ValidationProblem en las API mínimas requería que las propiedades errors y extensions se inicializaran con una implementación de IDictionary<string, object?>. En esta versión, estas API de construcción admiten sobrecargas que consumen 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);
});

Gracias al usuario de GitHub joegoldman2 por esta contribución.

Autenticación y autorización

En esta sección se describen las nuevas características de autenticación y autorización.

OpenIdConnectHandler agrega compatibilidad con solicitudes de autorización lanzadas (PAR)

Nos gustaría dar las gracias a Joe DeCock de Duende Software por agregar solicitudes de autorización lanzadas (PAR) a OpenIdConnectHandler de ASP.NET Core. Joe ha descrito los motivos y el trasfondo para habilitar PAR en su propuesta de API de la siguiente manera:

Las solicitudes de autorización lanzadas (PAR) son un estándar OAuth relativamente nuevo que mejora la seguridad de los flujos de OAuth y OIDC al mover los parámetros de autorización del canal frontal al canal posterior. Es decir, mover parámetros de autorización de direcciones URL de redirección en el explorador para dirigir las llamadas HTTP de la máquina al back-end.

Esto impide que un ciberdelincuente del explorador:

  • Vea los parámetros de autorización, lo que podría filtrar DCP.
  • Manipulación de esos parámetros. Por ejemplo, el ciberdelincuente podría cambiar el ámbito de acceso que se solicita.

La inserción de los parámetros de autorización también mantiene las direcciones URL de solicitud cortas. Los parámetros de autorización pueden llegar a ser muy largos cuando se usan características más complejas de OAuth y OIDC como las solicitudes de autorización enriquecidas. Las direcciones URL que son largas causan problemas en muchos exploradores e infraestructuras de red.

El grupo de trabajo FAPI de la fundación OpenID fomenta el uso de PAR. Por ejemplo, el perfil de seguridad FAPI2.0 requiere el uso de PAR. Muchos de los grupos que trabajan en banca abierta usan este perfil de seguridad (principalmente en Europa), en la atención sanitaria y en otros sectores con requisitos de alta seguridad.

PAR es compatible con varios proveedores de identity, incluyendo

Para .NET 9, hemos decidido habilitar PAR de forma predeterminada si el documento de detección del proveedor de identity anuncia compatibilidad con PAR, ya que debe proporcionar seguridad mejorada para los proveedores que la admiten. El documento de detección del proveedor de identity se encuentra normalmente en .well-known/openid-configuration. Si esto provoca problemas, puedes deshabilitar PAR a través de OpenIdConnectOptions.PushedAuthorizationBehavior de la siguiente manera:

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 asegurarte de que la autenticación solo se realiza correctamente si se usa PAR, usa PushedAuthorizationBehavior.Require en su lugar. Este cambio también presenta un nuevo evento OnPushAuthorization a OpenIdConnectEvents que se puede usar para personalizar la solicitud de autorización lanzada o controlarla manualmente. Consulta la propuesta de API para obtener más detalles.

Personalización de parámetros de OIDC y OAuth

Los gestores de autenticación OAuth y OIDC disponen ahora de una opción AdditionalAuthorizationParameters para facilitar la personalización de los parámetros de los mensajes de autorización que suelen incluirse como parte de la cadena de consulta de redireccionamiento. En .NET 8 y versiones anteriores, esto requiere una devolución de llamada personalizada OnRedirectToIdentityProvider o un método BuildChallengeUrl invalidado en un controlador personalizado. Este es un ejemplo de código de .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;
    };
});

El ejemplo anterior ahora se puede simplificar en el código siguiente:

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

Configuración de marcas de autenticación extendidas HTTP.sys

Ahora puede configurar las marcas HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING y HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL HTTP.sys utilizando las nuevas propiedades EnableKerberosCredentialCaching y CaptureCredentials en HTTP.sys AuthenticationManager para optimizar cómo se controla la autenticación de Windows. Por ejemplo:

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

Varios

En las secciones siguientes se describen varias características nuevas.

Nueva biblioteca de HybridCache

La API HybridCache cubre algunas lagunas en las API existentes IDistributedCache y IMemoryCache. También agrega nuevas funcionalidades, como:

  • Protección de "Stampede" para evitar capturas paralelas del mismo trabajo.
  • Serialización configurable.

HybridCache está diseñado como un reemplazo directo para el uso de IDistributedCache y IMemoryCache existentes, y proporciona una API sencilla para agregar código de almacenamiento en caché nuevo. Proporciona una API unificada para el almacenamiento en caché en proceso y fuera de proceso.

Para ver cómo se simplifica la API HybridCache, compárela con el código que usa IDistributedCache. Este es un ejemplo del aspecto que tiene el 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)
    { /* ... */ }
}

Eso supone mucho trabajo para conseguir hacerlo bien cada vez, incluyendo cosas como la serialización. Y en el escenario de pérdida de caché, podría acabar con varios subprocesos simultáneos, obtener una falta de caché, capturar todos los datos subyacentes, serializarlos y enviar esos datos a la memoria caché.

Para simplificar y mejorar este código con HybridCache, primero es necesario agregar la nueva biblioteca Microsoft.Extensions.Caching.Hybrid:

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

Registre el servicio HybridCache, como lo haría para registrar una implementación de IDistributedCache:

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

Ahora la mayoría de los problemas de almacenamiento en caché se pueden descargar en 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
        );
    }
}

Proporcionamos una implementación concreta de la clase abstracta HybridCache a través de la inserción de dependencias, pero está diseñada para que los desarrolladores puedan proporcionar implementaciones personalizadas de la API. La implementación HybridCache se ocupa de todo lo relacionado con el almacenamiento en caché, incluido el control de operaciones simultáneas. El token cancel aquí representa la cancelación combinada de todos los llamadores simultáneos, no solo la cancelación del autor de la llamada que podemos ver (es decir, token).

Los escenarios de alto rendimiento se pueden optimizar aún más mediante el patrón TState para evitar cierta sobrecarga de variables capturadas y devoluciones de llamada por instancia:

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 la implementación IDistributedCache configurada, si existe, para el almacenamiento en caché secundario fuera de proceso, por ejemplo, mediante Redis. Sin embargo, incluso sin IDistributedCache, el servicio HybridCache seguirá proporcionando protección de almacenamiento en caché en proceso y "stampede".

Nota sobre la reutilización de objetos

En el código existente típico que usa IDistributedCache, cada recuperación de un objeto de la memoria caché da como resultado la deserialización. Este comportamiento significa que cada llamador simultáneo obtiene una instancia independiente del objeto, que no puede interactuar con otras instancias. El resultado es la seguridad de subprocesos, ya que no hay ningún riesgo de modificaciones simultáneas en la misma instancia de objeto.

Dado que mucho uso de HybridCache se adaptará del código de IDistributedCache existente, HybridCache conserva este comportamiento de forma predeterminada para evitar introducir errores de simultaneidad. Sin embargo, un caso de uso determinado es intrínsecamente seguro para subprocesos:

  • Si los tipos que se almacenan en caché son inmutables.
  • Si el código no los modifica.

En tales casos, informe a HybridCache de que es seguro reutilizar instancias mediante:

  • Marcación del tipo como sealed. La palabra clave sealed en C# significa que la clase no se puede heredar.
  • Aplicación del atributo [ImmutableObject(true)] a él. El atributo [ImmutableObject(true)] indica que el estado del objeto no se puede cambiar después de crearlo.

Al reutilizar instancias, HybridCache puede reducir la sobrecarga de las asignaciones de CPU y objetos asociadas a la deserialización por llamada. Esto puede provocar mejoras de rendimiento en escenarios en los que los objetos almacenados en caché son grandes o a los que se accede con frecuencia.

Otras características de HybridCache

Al igual que IDistributedCache, HybridCache admite la eliminación por clave con un método RemoveKeyAsync.

HybridCache también proporciona API opcionales para implementaciones de IDistributedCache, con el fin de evitar asignaciones de byte[]. Esta característica se implementa mediante las versiones preliminares de los paquetes de Microsoft.Extensions.Caching.StackExchangeRedis y Microsoft.Extensions.Caching.SqlServer.

La serialización se configura como parte del registro del servicio, con compatibilidad con serializadores específicos de tipos y generalizados a través de los métodos WithSerializer y .WithSerializerFactory, encadenados desde la llamada AddHybridCache. De forma predeterminada, la biblioteca controla string y byte[] internamente, y usa System.Text.Json para todo lo demás, pero puede usar protobuf, xml o cualquier otra cosa.

HybridCache admite entornos de ejecución de .NET anteriores, hasta .NET Framework 4.7.2 y .NET Standard 2.0.

Para obtener más información sobre HybridCache, consulte Biblioteca HybridCache en ASP.NET Core

Mejoras en la página de excepciones para desarrolladores

La página de excepciones para desarrolladores de ASP.NET Core se muestra cuando una aplicación produce una excepción no controlada durante el desarrollo. La página de excepciones para desarrolladores proporciona información detallada sobre la excepción y la solicitud.

La versión preliminar 3 ha agregado metadatos de punto de conexión a la página de excepciones para desarrolladores. ASP.NET Core usa metadatos de punto de conexión para controlar el comportamiento del punto de conexión, como el enrutamiento, el almacenamiento en caché de respuestas, la limitación de velocidad, la generación de OpenAPI, etc. En la imagen siguiente, se muestra la nueva información de metadatos en la sección Routing de la página de excepciones para desarrolladores:

Nueva información de metadatos en la página de excepciones para desarrolladores

Al probar la página de excepciones para desarrolladores, se han identificado pequeñas mejoras de calidad de vida. Se han publicado en la versión preliminar 4:

  • Mejor ajuste de texto. Las cookies largas, los valores de cadena de consulta y los nombres de los métodos ya no agregan barras de desplazamiento horizontal en el navegador.
  • Texto más grande que se encuentra en diseños modernos.
  • Tamaños de tabla más coherentes.

En la imagen animada siguiente, se muestra la nueva página de excepciones para desarrolladores:

La nueva página de excepciones para desarrolladores

Mejoras en la depuración de diccionarios

La visualización de la depuración de los diccionarios y otras colecciones de clave-valor tiene un diseño mejorado. La clave se muestra en la columna de clave del depurador, en lugar de estar concatenada con el valor. Las imágenes siguientes muestran la presentación antigua y nueva de un diccionario en el depurador.

Antes:

La experiencia anterior del depurador

Después:

La nueva experiencia del depurador

ASP.NET Core tiene muchas colecciones de clave-valor. Esta experiencia de depuración mejorada se aplica a:

  • Encabezados HTTP
  • Cadenas de consulta
  • Formularios
  • Cookies
  • Visualización de datos
  • Datos de ruta
  • Características

Corrección para 503 durante el reciclaje de aplicaciones en IIS

De forma predeterminada, ahora hay un retraso de 1 segundo entre cuando IIS recibe una notificación de reciclaje o apagado y cuando ANCM indica al servidor administrado que empiece a apagarse. El retraso se puede configurar a través de la variable de entorno ANCM_shutdownDelay o al establecer la configuración del controlador shutdownDelay. Ambos valores están en milisegundos. El retraso es principalmente para reducir la probabilidad de una carrera donde:

  • IIS no ha iniciado las solicitudes de puesta en cola para ir a la nueva aplicación.
  • ANCM comienza a rechazar nuevas solicitudes que entran en la aplicación antigua.

Las máquinas más lentas o con mayor uso de CPU pueden querer ajustar este valor para reducir la probabilidad de 503.

Ejemplo de configuración 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>

La corrección está en el módulo ANCM instalado globalmente que procede de la agrupación de hospedaje.

Optimización de la entrega de recursos web estáticos

Las siguientes prácticas recomendadas de producción para atender recursos estáticos requieren una cantidad significativa de trabajo y conocimientos técnicos. Sin optimizaciones como la compresión, el almacenamiento en caché y las huellas digitales:

  • El explorador tiene que realizar solicitudes adicionales en cada carga de página.
  • Se transfieren más bytes de los necesarios a través de la red.
  • A veces, las versiones obsoletas de los archivos se sirven a los clientes.

La creación de aplicaciones web de rendimiento requiere la optimización de la entrega de recursos al explorador. Entre las posibles optimizaciones se incluyen:

  • Servir un recurso determinado una vez hasta que el archivo cambie o el explorador borre su caché. Establecer el encabezado ETag.
  • Impedir que el explorador use activos antiguos u obsoletos después de actualizar una aplicación. Establecer el encabezado Última modificación.
  • Configurar los encabezados de almacenamiento en caché adecuados.
  • Usar middleware de almacenamiento en caché.
  • Servir versiones comprimidas de los recursos siempre que sea posible.
  • Usar una red CDN para atender los recursos más cerca del usuario.
  • Minimizar el tamaño de los recursos servidos al explorador. Esta optimización no incluye la minificación.

MapStaticAssets es un nuevo middleware que ayuda a optimizar la entrega de recursos estáticos en una aplicación. Está diseñado para trabajar con todos los marcos de interfaz de usuario, como Blazor, Razor Pages y MVC. Normalmente, es un reemplazo de drop-in para 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 funciona combinando procesos en tiempo de compilación y publicación para recopilar información sobre todos los recursos estáticos de una aplicación. A continuación, la biblioteca en tiempo de ejecución utiliza esta información para servir estos archivos de forma eficaz al explorador.

MapStaticAssets puede reemplazar UseStaticFiles en la mayoría de las situaciones, pero está optimizado para atender los recursos de los que la aplicación tiene conocimiento en tiempo de compilación y publicación. Si la aplicación atiende recursos de otras ubicaciones, como el disco o los recursos incrustados, debe usarse UseStaticFiles.

MapStaticAssets proporciona las siguientes ventajas no encontradas con UseStaticFiles:

  • Compresión en tiempo de compilación para todos los recursos de la aplicación:
    • gzip durante el desarrollo y gzip + brotli durante la publicación.
    • Todos los recursos se comprimen con el objetivo de reducir el tamaño de los recursos al mínimo.
  • ETags basado en contenido: el Etags para cada recurso es la cadena codificada en Base64 del hash SHA-256 del contenido. Esto garantiza que el explorador solo vuelva a descargar un archivo si su contenido ha cambiado.

En la tabla siguiente se muestran los tamaños originales y comprimidos de los archivos CSS y JS en la plantilla predeterminada de Razor Pages:

Archivo Original Compressed % de reducción
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%

En la tabla siguiente se muestran los tamaños originales y comprimidos mediante la biblioteca de componentes de la interfaz de usuario de Fluent Blazor:

Archivo Original Compressed % de reducción
fluent.js 384 73 80.99%
fluent.css 94 11 88,30%
Total 478 84 82,43%

Para un total de 478 KB sin comprimir a 84 KB comprimidos.

En la tabla siguiente se muestran los tamaños originales y comprimidos mediante la biblioteca de componentes MudBlazorBlazor:

Archivo Original Compressed Reducción
MudBlazor.min.css 541 37,5 93,07%
MudBlazor.min.js 47,4 9.2 80,59%
Total 588,4 46,7 92,07%

La optimización se produce automáticamente cuando se usa MapStaticAssets. Cuando se agrega o actualiza una biblioteca, por ejemplo con JavaScript o CSS nuevos, los recursos se optimizan como parte de la compilación. La optimización es especialmente beneficiosa para los entornos móviles que pueden tener un ancho de banda menor o conexiones no confiables.

Para obtener más información sobre las nuevas características de entrega de archivos, consulte los siguientes recursos:

Habilitación de la compresión dinámica en el servidor frente al uso de MapStaticAssets

MapStaticAssets tiene las siguientes ventajas sobre la compresión dinámica en el servidor:

  • Es más sencillo porque no hay ninguna configuración específica del servidor.
  • Es más eficaz porque los recursos se comprimen en tiempo de compilación.
  • Permite al desarrollador dedicar más tiempo durante el proceso de compilación para asegurarse de que los recursos tengan el tamaño mínimo.

Ten en cuenta la tabla siguiente que compara la compresión MudMudBlazor con la compresión dinámica de IIS y MapStaticAssets:

IIS gzip MapStaticAssets Reducción de MapStaticAssets
≅ 90 37,5 59 %

ASP0026: analizador para advertir cuando [Authorize] está invalidado por [AllowAnonymous] desde "más lejos"

Parece intuitivo que un atributo [Authorize] colocado "más cerca" de una acción de MVC que un atributo [AllowAnonymous] invalidaría el atributo [AllowAnonymous] y forzaría la autorización. Sin embargo, esto no tiene por qué ser así. Lo que importa es el orden relativo de los atributos.

En el siguiente código se muestran ejemplos en los que un atributo [Authorize] más cercano se invalida mediante un atributo [AllowAnonymous] que está más lejos.

[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
{
}

En la versión preliminar 6 de .NET 9, hemos introducido un analizador que resaltará instancias como estas, donde un atributo [Authorize] más cercano se invalida mediante un atributo [AllowAnonymous] que está más lejos de una acción de MVC. La advertencia apunta al atributo [Authorize] invalidado con el mensaje siguiente:

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

La acción correcta que se debe realizar si ve esta advertencia depende de la intención detrás de los atributos. El atributo [AllowAnonymous] más alejado debe quitarse si expone involuntariamente el punto de conexión a usuarios anónimos. Si el atributo [AllowAnonymous] estaba diseñado para invalidar un atributo [Authorize] más cercano, puede repetir el atributo [AllowAnonymous] después del atributo [Authorize] para aclarar la intención.

[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 conexión Kestrel mejoradas

Hemos realizado una mejora significativa de las métricas de conexión de Kestrel al incluir metadatos sobre por qué se ha producido un error de conexión. La métrica kestrel.connection.duration ahora incluye el motivo de cierre de la conexión del atributo error.type.

He aquí una pequeña muestra de los valores error.type:

  • tls_handshake_failed - La conexión requiere TLS y se ha producido un error en el protocolo de enlace TLS.
  • connection_reset - El cliente ha cerrado inesperadamente la conexión mientras las solicitudes estaban en curso.
  • request_headers_timeout : Kestrel ha cerrado la conexión porque no ha recibido encabezados de solicitud a tiempo.
  • max_request_body_size_exceeded : Kestrel ha cerrado la conexión porque los datos cargados han superado el tamaño máximo.

Anteriormente, para diagnosticar problemas de conexión Kestrel era necesario que el servidor registrara datos detallados de bajo nivel. Sin embargo, los registros pueden ser costosos de generar y almacenar, y puede ser difícil encontrar la información correcta entre el ruido.

Las métricas son una alternativa mucho más barata que se puede dejar en un entorno de producción con un impacto mínimo. Las métricas recopiladas pueden impulsar paneles y alertas. Una vez que se identifica un problema en un nivel alto con métricas, puede comenzar una investigación adicional mediante el registro y otras herramientas.

Esperamos que las métricas de conexión mejoradas sean útiles en muchos escenarios:

  • Investigación de problemas de rendimiento causados por una corta duración de la conexión.
  • Observación de los ataques externos en curso sobre Kestrel que impactan en el rendimiento y la estabilidad.
  • Grabación de intentos de ataques externos a Kestrel que el fortalecimiento de la seguridad integrada de Kestrel ha impedido.

Para obtener más información, consulta métricas de ASP.NET Core.

Personalizción de puntos de conexión de canalización con nombre Kestrel

Se ha mejorado la compatibilidad con canalizaciones con nombre de Kestrel con opciones de personalización avanzadas. El nuevo método CreateNamedPipeServerStream de las opciones de canalización con nombre permite personalizar las canalizaciones por punto de conexión.

Un ejemplo de dónde resulta útil es una aplicación Kestrel que requiera dos puntos de conexión de canalización con diferente seguridad de acceso. La opción CreateNamedPipeServerStream se puede usar para crear canalizaciones con configuraciones de seguridad personalizadas, en función del nombre de la canalización.

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);
    };
});

Opción ExceptionHandlerMiddleware para elegir el código de estado en función del tipo de excepción

Una nueva opción al configurar ExceptionHandlerMiddleware permite a los desarrolladores de aplicaciones elegir qué código de estado devolver cuando se produce una excepción durante el control de solicitudes. La nueva opción cambia el código de estado que se establece en la respuesta ProblemDetails de ExceptionHandlerMiddleware.

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

No participar en métricas HTTP en determinados puntos de conexión y solicitudes

.NET 9 presenta la capacidad de no participar en las métricas HTTP de puntos de conexión y solicitudes específicos. No participar en la grabación de métricas es beneficioso para los puntos de conexión a los que llaman con frecuencia los sistemas automatizados, como las comprobaciones de estado. La grabación de métricas para estas solicitudes suele ser innecesaria.

Las solicitudes HTTP a un punto de conexión se pueden excluir de las métricas agregando metadatos. Opciones:

  • Agrega el atributo [DisableHttpMetrics] al controlador de la API web, al centro de conectividad SignalR o al servicio gRPC.
  • Llama a DisableHttpMetrics al asignar puntos finales en el inicio de la aplicación:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

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

La propiedad MetricsDisabled se ha agregado a IHttpMetricsTagsFeature para:

  • Escenarios avanzados en los que una solicitud no se asigna a un punto de conexión.
  • Deshabilita dinámicamente la recopilación de métricas para solicitudes 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);
});

Compatibilidad con la protección de datos para eliminar claves

Antes de .NET 9, las claves de protección de datos no se podían borrar por diseño, para evitar la pérdida de datos. La eliminación de una clave representa sus datos protegidos irrecuperables. Dado su tamaño pequeño, la acumulación de estas claves suele suponer un impacto mínimo. Sin embargo, para dar cabida a servicios extremadamente de larga duración, hemos introducido la opción de eliminar claves. Por lo general, solo se deben eliminar las claves antiguas. Elimina solo las claves cuando puedas aceptar el riesgo de pérdida de datos a cambio de ahorros de almacenamiento. Recomendamos que las claves de protección de datos no se eliminen.

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.");
}

El middleware es compatible con DI con claves

El middleware es ahora compatible con DI con claves en el constructor y el 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);
}

Confianza en el certificado de desarrollo HTTPS de ASP.NET Core en Linux

En distribuciones de Linux basadas en Ubuntu y Fedora, dotnet dev-certs https --trust ahora configura un certificado de desarrollo HTTPS de ASP.NET Core como certificado de confianza para:

  • Navegadores Chromium, por ejemplo, Google Chrome, Microsoft Edge y Chromium.
  • Mozilla Firefox y Mozilla navegadores derivados.
  • API de .NET, por ejemplo, HttpClient

Anteriormente, --trust solo funcionaba en Windows y macOS. La confianza de certificado se aplica por usuario.

Para establecer la confianza en OpenSSL, la herramienta dev-certs:

  • Coloca el certificado en ~/.aspnet/dev-certs/trust
  • Ejecuta una versión simplificada de la herramienta c_rehash de OpenSSL en el directorio.
  • Pide al usuario que actualice la variable de entorno SSL_CERT_DIR.

Para establecer la confianza en dotnet, la herramienta coloca el certificado en el almacén de certificados My/Root.

Para establecer la confianza en las bases de datos NSS, si las hay, la herramienta busca en el directorio de home los perfiles de Firefox, ~/.pki/nssdb y ~/snap/chromium/current/.pki/nssdb. Para cada directorio encontrado, la herramienta agrega una entrada a nssdb.