Novedades de .NET 9

Obtenga información sobre las nuevas características de .NET 9 y busque vínculos a documentación adicional.

.NET 9, el sucesor de .NET 8, tiene un enfoque especial en el rendimiento y las aplicaciones nativas de la nube. Se admitirá durante 18 meses como versión de soporte técnico estándar (STS). Puede descargar .NET 9 aquí.

Novedad de .NET 9: el equipo de ingeniería publica actualizaciones en versión preliminar de .NET 9 en GitHub Discussions. Este es un excelente lugar para hacer preguntas y proporcionar comentarios sobre la versión.

Este artículo se ha actualizado para .NET 9 Preview 2. En las siguientes secciones se describen las actualizaciones de las bibliotecas básicas de .NET en .NET 9.

Entorno de ejecución de .NET

Serialización

En System.Text.Json, .NET 9 tiene nuevas opciones para serializar JSON y un nuevo singleton que facilita la serialización mediante valores predeterminados web.

Opciones de sangría

JsonSerializerOptions incluye nuevas propiedades que permiten personalizar el carácter y el tamaño de sangría de JSON escrito.

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 2,
};

string json = JsonSerializer.Serialize(
    new { Value = 1 },
    options
    );
Console.WriteLine(json);
//{
//                "Value": 1
//}

Opciones web predeterminadas

Si desea serializar con las opciones predeterminadas que ASP.NET Core usa para las aplicaciones web, use el nuevo singleton JsonSerializerOptions.Web.

string webJson = JsonSerializer.Serialize(
    new { SomeValue = 42 },
    JsonSerializerOptions.Web // Defaults to camelCase naming policy.
    );
Console.WriteLine(webJson);
// {"someValue":42}

LINQ

Se han introducido nuevos métodos CountBy y AggregateBy. Estos métodos permiten agregar el estado por clave sin necesidad de asignar agrupaciones intermedias a través de GroupBy.

CountBy permite calcular rápidamente la frecuencia de cada clave. En el siguiente ejemplo se busca la palabra que se produce con más frecuencia en una cadena de texto.

string sourceText = """
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    Sed non risus. Suspendisse lectus tortor, dignissim sit amet, 
    adipiscing nec, ultricies sed, dolor. Cras elementum ultrices amet diam.
""";

// Find the most frequent word in the text.
KeyValuePair<string, int> mostFrequentWord = sourceText
    .Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries)
    .Select(word => word.ToLowerInvariant())
    .CountBy(word => word)
    .MaxBy(pair => pair.Value);

Console.WriteLine(mostFrequentWord.Key); // amet

AggregateBy le permite implementar flujos de trabajo de uso más general. En el siguiente ejemplo se muestra cómo se pueden calcular las puntuaciones asociadas a una clave determinada.

(string id, int score)[] data =
    [
        ("0", 42),
        ("1", 5),
        ("2", 4),
        ("1", 10),
        ("0", 25),
    ];

var aggregatedData =
    data.AggregateBy(
        keySelector: entry => entry.id,
        seed: 0,
        (totalScore, curr) => totalScore + curr.score
        );

foreach (var item in aggregatedData)
{
    Console.WriteLine(item);
}
//(0, 67)
//(1, 15)
//(2, 4)

Index<TSource>(IEnumerable<TSource>) permite extraer rápidamente el índice implícito de un enumerable. Ahora puede escribir código como el siguiente fragmento de código para indexar automáticamente los elementos de una colección.

IEnumerable<string> lines2 = File.ReadAllLines("output.txt");
foreach ((int index, string line) in lines2.Index())
{
    Console.WriteLine($"Line number: {index + 1}, Line: {line}");
}

Colecciones

El tipo de colección PriorityQueue<TElement,TPriority> en el espacio de nombres System.Collections.Generic incluye un nuevo método Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) que puede usar para actualizar la prioridad de un elemento de la cola.

Método PriorityQueue.Remove()

.NET 6 introdujo la colección PriorityQueue<TElement,TPriority>, que proporciona una implementación de montón de matriz simple y rápida. Un problema con los montones de matriz en general es que no admiten actualizaciones de prioridad, lo que hace que sean prohibitivos para su uso en algoritmos como variaciones del algoritmo de Dijkstra.

Aunque no es posible implementar actualizaciones de prioridad $O(\log n)$ eficaces en la colección existente, el nuevo método PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) permite emular las actualizaciones de prioridad (aunque en tiempo $O(n)$):

public static void UpdatePriority<TElement, TPriority>(
    this PriorityQueue<TElement, TPriority> queue,
    TElement element,
    TPriority priority
    )
{
    // Scan the heap for entries matching the current element.
    queue.Remove(element, out _, out _);
    // Re-insert the entry with the new priority.
    queue.Enqueue(element, priority);
}

Este método desbloquea a los usuarios que desean implementar algoritmos de grafos en contextos en los que el rendimiento asintótico no es un obstáculo. (Estos contextos incluyen educación y creación de prototipos). Por ejemplo, esta es una implementación de toy del algoritmo de Dijkstra que usa la nueva API.

Criptografía

Para la criptografía, .NET 9 agrega un nuevo método hash de captura única en el tipo CryptographicOperations. También agrega nuevas clases que usan el algoritmo KMAC.

Método CryptographicOperations.HashData()

.NET incluye varias implementaciones estáticas "de un solo uso" de funciones hash y funciones relacionadas. Estas API incluyen SHA256.HashData y HMACSHA256.HashData. Las API de un solo uso son preferibles, ya que pueden proporcionar el mejor rendimiento posible y reducir o eliminar asignaciones.

Si un desarrollador quiere proporcionar una API que admita el hash en el que el autor de la llamada define qué algoritmo hash se va a usar, normalmente se realiza aceptando un argumento HashAlgorithmName. Sin embargo, el uso de ese patrón con API de un solo uso requeriría cambiar cada uno de los HashAlgorithmName posibles y, a continuación, usar el método adecuado. Para solucionar ese problema, .NET 9 presenta la API CryptographicOperations.HashData. Esta API le permite generar un hash o HMAC a través de una entrada como una captura única en la que un algoritmo utilizado viene determinado por HashAlgorithmName.

static void HashAndProcessData(HashAlgorithmName hashAlgorithmName, byte[] data)
{
    byte[] hash = CryptographicOperations.HashData(hashAlgorithmName, data);
    ProcessHash(hash);
}

Algoritmo KMAC

.NET 9 proporciona el algoritmo KMAC especificado por NIST SP-800-185. El código de autenticación de mensajes (KMAC) de KECCAK es una función pseudoaleatoria y una función hash con clave basada en KECCAK.

Las siguientes clases nuevas usan el algoritmo KMAC. Use instancias para acumular datos para generar un MAC o usar el método estático HashData para una única entrada.

KMAC está disponible en Linux con OpenSSL 3.0 o posterior, y en la compilación 26016 o posterior de Windows 11. Puede usar la propiedad estática IsSupported para determinar si la plataforma admite el algoritmo deseado.

if (Kmac128.IsSupported)
{
    byte[] key = GetKmacKey();
    byte[] input = GetInputToMac();
    byte[] mac = Kmac128.HashData(key, input, outputLength: 32);
}
else
{
    // Handle scenario where KMAC isn't available.
}

Reflexión

En las versiones de .NET Core y .NET 5-8, la compatibilidad con la creación de un ensamblado y la emisión de metadatos de reflexión para tipos creados dinámicamente se limitaba a un ejecutable AssemblyBuilder. La falta de compatibilidad para guardar un ensamblado suele ser un obstáculo para los clientes que migran de .NET Framework a .NET. .NET 9 agrega API públicas para AssemblyBuilder para guardar un ensamblado emitido.

La nueve implementación AssemblyBuilder persistente es independiente del entorno de ejecución y de la plataforma. Para crear una instancia AssemblyBuilder persistente, use la nueva API AssemblyBuilder.DefinePersistedAssembly. La API AssemblyBuilder.DefineDynamicAssembly existente acepta el nombre del ensamblado y los atributos personalizados opcionales. Para usar la nueva API, pase el ensamblado principal, System.Private.CoreLib, que se usa para hacer referencia a los tipos de tiempo de ejecución base. No hay ninguna opción para AssemblyBuilderAccess. Y por ahora, la implementación AssemblyBuilder persistente solo admite guardar, no ejecutar. Después de crear una instancia AssemblyBuilder persistente, los pasos posteriores para definir un módulo, tipo, método o enumeración, escribir lenguaje intermedio y todos los demás usos permanecen sin cambios. Esto significa que puede usar el código System.Reflection.Emit existente tal como está para guardar el ensamblado. El código siguiente muestra un ejemplo.

public void CreateAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(
        new AssemblyName("MyAssembly"),
        typeof(object).Assembly
        );
    TypeBuilder tb = ab.DefineDynamicModule("MyModule")
        .DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);

    MethodBuilder mb = tb.DefineMethod(
        "SumMethod",
        MethodAttributes.Public | MethodAttributes.Static,
        typeof(int), [typeof(int), typeof(int)]
        );
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();
    ab.Save(assemblyPath); // or could save to a Stream
}

public void UseAssembly(string assemblyPath)
{
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    Type type = assembly.GetType("MyType");
    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, [5, 10]));
}

Rendimiento

.NET 9 incluye mejoras en el compilador JIT de 64 bits destinado a mejorar el rendimiento de la aplicación. Estas mejoras del compilador incluyen:

La vectorización de Arm64 es otra nueva característica del runtime.

Optimizaciones de bucles

Mejorar la generación de código para bucles es una prioridad de .NET 9, y el compilador de 64 bits incorpora una nueva optimización denominada ampliación de variables de inducción (IV).

Un IV es una variable cuyo valor cambia a medida que el bucle contenedor recorre en iteración. En el siguiente bucle for, i es un IV: for (int i = 0; i < 10; i++). Si el compilador puede analizar cómo evoluciona el valor de un IV a través de las iteraciones de su bucle, puede generar código más eficaz para expresiones relacionadas.

Considere el siguiente ejemplo que recorre en iteración una matriz:

static int Sum(int[] arr)
{
    int sum = 0;
    for (int i = 0; i < arr.Length; i++)
    {
        sum += arr[i];
    }

    return sum;
}

La variable de índice, i, tiene un tamaño de 4 bytes. En el nivel de ensamblado, los registros de 64 bits se usan normalmente para contener índices de matriz en x64 y en versiones anteriores de .NET, el código generado por el compilador que extendía i de cero a 8 bytes para el acceso a la matriz, pero continuó tratando i como un entero de 4 bytes en otro lugar. Sin embargo, extender i a 8 bytes requiere una instrucción adicional en x64. Con el ensanchamiento IV, el compilador JIT de 64 bits ahora amplía i a 8 bytes en todo el bucle, omitiendo la extensión cero. El bucle sobre matrices es muy común y las ventajas de esta eliminación de instrucciones se suman rápidamente.

Mejoras de inserción para AOT nativo

Uno de los objetivos de .NET para la inserción del compilador JIT de 64 bits es eliminar el mayor número posible de restricciones que impiden insertar un método. .NET 9 permite insertar accesos a estáticas locales de subprocesos en Windows x64, Linux x64 y Linux Arm64.

Para miembros de la clase static, existe exactamente una instancia del miembro en todas las instancias de la clase, que "comparten" el miembro. Si el valor de un miembro static es único para cada subproceso, hacer que ese valor local del subproceso pueda mejorar el rendimiento, ya que elimina la necesidad de que un primitivo de simultaneidad acceda de forma segura al miembro static desde su subproceso contenedor.

Anteriormente, los accesos a estáticas locales de subprocesos en programas compilados con AOT nativo requerían que el compilador JIT de 64 bits emitiera una llamada al runtime para obtener la dirección base del almacenamiento local de subprocesos. Ahora, el compilador puede insertar estas llamadas, lo que da lugar a muchas menos instrucciones para acceder a estos datos.

Mejoras de PGO: comprobaciones de tipos y conversiones

.NET 8 permitía por defecto la optimización dinámica guiada por perfiles (PGO). NET 9 amplía la implementación de la PGO del compilador JIT de 64 bits para perfilar más patrones de código. Cuando la compilación en capas está habilitada, el compilador JIT de 64 bits ya inserta instrumentación en el programa para generar perfiles de su comportamiento. Cuando se vuelve a compilar con optimizaciones, el compilador aprovecha el perfil que creó en runtime para tomar decisiones específicas de la ejecución actual del programa. En .NET 9, el compilador JIT de 64 bits usa datos PGO para mejorar el rendimiento de las comprobaciones de tipos.

La determinación del tipo de un objeto requiere una llamada al tiempo de ejecución, lo que conlleva una penalización de rendimiento. Cuando es necesario comprobar el tipo de un objeto, el compilador JIT de 64 bits emite esta llamada por motivos de corrección (los compiladores normalmente no pueden descartar ninguna posibilidad, aunque parezcan improbables). Sin embargo, si los datos de PGO sugieren que un objeto es probable que sea un tipo específico, el compilador JIT de 64 bits ahora emite una ruta de acceso rápida que comprueba de forma barata ese tipo y retrocede en la ruta de acceso lenta de llamar al entorno de ejecución solo si es necesario.

Vectorización Arm64 en bibliotecas de .NET

Una nueva implementación EncodeToUtf8 aprovecha la capacidad del compilador JIT de 64 bits para emitir instrucciones de carga y almacenamiento de varios registros en Arm64. Este comportamiento permite a los programas procesar fragmentos de datos más grandes con menos instrucciones. Las aplicaciones .NET en varios dominios deben ver mejoras de rendimiento en el hardware Arm64 que admite estas características. Algunos punto de referencia reducen el runtime en más de la mitad.

.NET SDK

Pruebas unitarias

En esta sección se describen las actualizaciones de las pruebas unitarias en .NET 9: ejecución de pruebas en paralelo y salida de prueba del registrador de terminales.

Ejecución de pruebas en paralelo

En .NET 9, dotnet test está totalmente integrado con MSBuild. Dado que MSBuild admite la compilación en paralelo, puede ejecutar pruebas para el mismo proyecto en diferentes marcos de destino en paralelo. De forma predeterminada, MSBuild limita el número de procesos paralelos al número de procesadores del equipo. También puede establecer su propio límite mediante el modificador -maxcpucount. Si desea no participar en el paralelismo, establezca la propiedad TestTfmsInParallel de MSBuild en false.

Pantalla de prueba del registrador de terminal

Los informes de resultados de pruebas para dotnet test ahora se admiten directamente en el registrador de terminales de MSBuild. Obtendrá informes de pruebas más completos tanto mientras se ejecutan pruebas (muestra el nombre de la prueba en ejecución) y después de que se completen las pruebas (los errores de prueba se representan de una manera mejor).

Para obtener más información sobre el registrador de terminales, consulte las opciones de compilación de dotnet.

Puesta al día de la herramienta .NET

Las herramientas de .NET son aplicaciones dependientes del marco que se pueden instalar de forma global o local y, después, se ejecutan con el SDK de .NET y los runtime de .NET instalados. Estas herramientas, como todas las aplicaciones de .NET, tienen como destino una versión principal específica de .NET. De forma predeterminada, las aplicaciones no se ejecutan en versiones más recientes de .NET. Los autores de herramientas han podido optar por ejecutar sus herramientas en versiones más recientes del entorno de ejecución de .NET estableciendo la propiedad RollForward de MSBuild. Sin embargo, no todas las herramientas lo hacen.

Una nueva opción para dotnet tool install permite a los usuarios decidir cómo se deben ejecutar las herramientas de .NET. Al instalar una herramienta a través de dotnet tool install, o al ejecutarla mediante dotnet tool run <toolname>, puede especificar una nueva marca denominada --allow-roll-forward. Esta opción configura la herramienta con el modo de puesta al día Major. Este modo permite que la herramienta se ejecute en una versión principal más reciente de .NET si la versión de .NET coincidente no está disponible. Esta característica ayuda a los primeros usuarios a usar herramientas de .NET sin que los autores de herramientas tengan que cambiar ningún código.

Consulte también