Compartir a través de


Novedades de las bibliotecas de .NET para .NET 9

En este artículo se describen las nuevas características de las bibliotecas de .NET para .NET 9.

Base64Url

Base64 es un esquema de codificación que traduce bytes arbitrarios en texto compuesto por un conjunto específico de 64 caracteres. Se trata de un enfoque común para transferir datos y ha sido admitido durante mucho tiempo a través de una variedad de métodos, como con Convert.ToBase64String o Base64.DecodeFromUtf8(ReadOnlySpan<Byte>, Span<Byte>, Int32, Int32, Boolean). Sin embargo, algunos de los caracteres que utiliza hacen que no sea ideal para su uso en ciertas circunstancias en las que de otro modo te gustaría usarlo, como en cadenas de consulta. En concreto, los 64 caracteres que componen la tabla Base64 incluyen '+' y '/', ambos tienen su propio significado en las direcciones URL. Esto llevó a la creación del esquema Base64Url, que es similar a Base64, pero usa un conjunto ligeramente diferente de caracteres que lo hace adecuado para su uso en contextos de direcciones URL. .NET 9 incluye la nueva Base64Url clase , que proporciona muchos métodos útiles y optimizados para codificar y descodificar con Base64Url a y desde una variedad de tipos de datos.

En el ejemplo siguiente se muestra el uso de la nueva clase .

ReadOnlySpan<byte> bytes = ...;
string encoded = Base64Url.EncodeToString(bytes);

Formateador binario

.NET 9 quita BinaryFormatter del entorno de ejecución de .NET. Las APIs siguen estando presentes, pero sus implementaciones siempre lanzan una excepción, independientemente del tipo de proyecto. Para obtener más información sobre la eliminación y las opciones si se ve afectado, consulte La guía de migración de BinaryFormatter.

Colecciones

Los tipos de colección de .NET obtienen las siguientes actualizaciones para .NET 9:

Búsquedas de colecciones con intervalos

En el código de alto rendimiento, las spans a menudo se utilizan para evitar asignar cadenas de texto innecesariamente, y las tablas de búsqueda con tipos como Dictionary<TKey,TValue> y HashSet<T> se utilizan con frecuencia como cachés. Sin embargo, no ha habido ningún mecanismo integrado seguro para realizar búsquedas en estos tipos de colección con intervalos. Con la nueva allows ref struct característica de C# 13 y las nuevas características de estos tipos de colección en .NET 9, ahora es posible realizar estos tipos de búsquedas.

En el ejemplo siguiente se muestra cómo usar Dictionary<TKey,TValue>.GetAlternateLookup.

static Dictionary<string, int> CountWords(ReadOnlySpan<char> input)
{
    Dictionary<string, int> wordCounts = new(StringComparer.OrdinalIgnoreCase);
    Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> spanLookup =
        wordCounts.GetAlternateLookup<ReadOnlySpan<char>>();

    foreach (Range wordRange in Regex.EnumerateSplits(input, @"\b\W+"))
    {
        if (wordRange.Start.Value == wordRange.End.Value)
        {
            continue; // Skip empty ranges.
        }
        ReadOnlySpan<char> word = input[wordRange];
        spanLookup[word] = spanLookup.TryGetValue(word, out int count) ? count + 1 : 1;
    }

    return wordCounts;
}

OrderedDictionary<TKey, TValue>

En muchos escenarios, es posible que quiera almacenar pares clave-valor de una manera en la que se pueda mantener el orden (una lista de pares clave-valor), pero donde también se admite la búsqueda rápida por clave (un diccionario de pares clave-valor). Desde los primeros días de .NET, el OrderedDictionary tipo ha admitido este escenario, pero solo de manera no genérica, con claves y valores tipados como object. .NET 9 presenta la muy solicitada colección OrderedDictionary<TKey,TValue>, que proporciona un tipo genérico eficaz para admitir estos escenarios.

El código siguiente usa la nueva clase .

OrderedDictionary<string, int> d = new()
{
    ["a"] = 1,
    ["b"] = 2,
    ["c"] = 3,
};

d.Add("d", 4);
d.RemoveAt(0);
d.RemoveAt(2);
d.Insert(0, "e", 5);

foreach (KeyValuePair<string, int> entry in d)
{
    Console.WriteLine(entry);
}

// Output:
// [e, 5]
// [b, 2]
// [c, 3]

Método PriorityQueue.Remove()

.NET 6 introdujo la PriorityQueue<TElement,TPriority> colección, 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 PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) método permite emular las actualizaciones de prioridad (aunque en $O(n)$ time):

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 asymptotico no es un bloqueador. (Estos contextos incluyen educación y creación de prototipos). Por ejemplo, esta es una implementación toy del algoritmo de Dijkstra que usa la nueva API.

ReadOnlySet<T>

A menudo, lo ideal es presentar vistas de solo lectura de las colecciones. ReadOnlyCollection<T> le permite crear un contenedor de solo lectura en torno a un IList<T> mutable arbitrario, y ReadOnlyDictionary<TKey,TValue> le permite crear un contenedor de solo lectura en torno a un IDictionary<TKey,TValue> mutable arbitrario. Sin embargo, las versiones anteriores de .NET no tenían compatibilidad integrada para hacer lo mismo con ISet<T>. .NET 9 presenta ReadOnlySet<T> para solucionar este problema.

La nueva clase habilita el siguiente patrón de uso.

private readonly HashSet<int> _set = [];
private ReadOnlySet<int>? _setWrapper;

public ReadOnlySet<int> Set => _setWrapper ??= new(_set);

Modelo de componente: compatibilidad de recorte con TypeDescriptor

System.ComponentModel incluye nuevas API opcionales que son compatibles con el recorte para describir componentes. Cualquier aplicación, especialmente las aplicaciones recortadas autónomas, puede utilizar estas nuevas API para incluir más usos de recorte.

La API principal es el TypeDescriptor.RegisterType método de la TypeDescriptor clase . Este método tiene el atributo DynamicallyAccessedMembersAttribute para que el recortador conserve los miembros de ese tipo. Debe llamar a este método una vez por tipo y, normalmente, al principio.

Las API secundarias tienen un FromRegisteredType sufijo, como TypeDescriptor.GetPropertiesFromRegisteredType(Type). A diferencia de sus homólogos que no tienen el FromRegisteredType sufijo, estas API no tienen [RequiresUnreferencedCode] ni [DynamicallyAccessedMembers] atributos de optimizador. Si no hay atributos del recortador, los consumidores podrán evitar lo siguiente:

  • Eliminar las advertencias de recorte, lo que puede ser un factor de riesgo.
  • Propagar un parámetro Type con tipado fuerte en otros métodos puede ser complicado o inviable.
public static void RunIt()
{
    // The Type from typeof() is passed to a different method.
    // The trimmer doesn't know about ExampleClass anymore
    // and thus there will be warnings when trimming.
    Test(typeof(ExampleClass));
    Console.ReadLine();
}

private static void Test(Type type)
{
    // When publishing self-contained + trimmed,
    // this line produces warnings IL2026 and IL2067.
    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(type);

    // When publishing self-contained + trimmed,
    // the property count is 0 here instead of 2.
    Console.WriteLine($"Property count: {properties.Count}");

    // To avoid the warning and ensure reflection
    // can see the properties, register the type:
    TypeDescriptor.RegisterType<ExampleClass>();
    // Get properties from the registered type.
    properties = TypeDescriptor.GetPropertiesFromRegisteredType(type);

    Console.WriteLine($"Property count: {properties.Count}");
}

public class ExampleClass
{
    public string? Property1 { get; set; }
    public int Property2 { get; set; }
}

Para obtener más información, consulte la propuesta de API de .

Criptografía

Método CryptographicOperations.HashData()

.NET incluye varias implementaciones estáticas de funciones hash y funciones relacionadas "de una sola vez". 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 HashAlgorithmName argumento. 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 CryptographicOperations.HashData API. Esta API le permite generar un hash o HMAC sobre una entrada como una operación única, donde el algoritmo utilizado es 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.
}

Los algoritmos AES-GCM y ChaChaPoly1305 están habilitados para iOS/tvOS/MacCatalyst

IsSupported y ChaChaPoly1305.IsSupported ahora devuelven el valor "true" cuando se ejecutan en iOS 13+, tvOS 13+ y Mac Catalyst.

AesGcm solo admite valores de etiqueta de 16 bytes (128 bits) en sistemas operativos Apple.

Carga de certificados X.509

Desde .NET Framework 2.0, la manera de cargar un certificado ha sido new X509Certificate2(bytes). También se han producido otros patrones, como new X509Certificate2(bytes, password, flags), new X509Certificate2(path), new X509Certificate2(path, password, flags)y X509Certificate2Collection.Import(bytes, password, flags) (y sus sobrecargas).

Todos esos métodos utilizaban el examen de contenido para determinar si la entrada era algo que podían manejar, y luego lo cargaban si podían. Para algunos autores de llamadas, esta estrategia era muy conveniente. Pero también tiene algunos problemas:

  • No todos los formatos de archivo funcionan en todos los sistemas operativos.
  • Es una desviación de protocolo.
  • Es una fuente de problemas de seguridad.

.NET 9 presenta una nueva X509CertificateLoader clase, que tiene un diseño de "un método, un propósito". En su versión inicial, solo admite dos de los cinco formatos admitidos por el X509Certificate2 constructor. Estos son los dos formatos que funcionaban en todos los sistemas operativos.

Compatibilidad con proveedores de OpenSSL

.NET 8 introdujo las API OpenPrivateKeyFromEngine(String, String) específicas de OpenSSL y OpenPublicKeyFromEngine(String, String). Permiten interactuar con los componentes de OpenSSL ENGINE y usan módulos de seguridad de hardware (HSM), por ejemplo.

.NET 9 presenta SafeEvpPKeyHandle.OpenKeyFromProvider(String, String), que permite usar proveedores de OpenSSL e interactuar con proveedores como tpm2 o pkcs11.

Algunas distribuciones han eliminado la compatibilidad con ENGINE porque ya está obsoleto.

En el fragmento de código siguiente se muestra el uso básico:

byte[] data = [ /* example data */ ];

// Refer to your provider documentation, for example, https://github.com/tpm2-software/tpm2-openssl/tree/master.
using (SafeEvpPKeyHandle priKeyHandle = SafeEvpPKeyHandle.OpenKeyFromProvider("tpm2", "handle:0x81000007"))
using (ECDsa ecdsaPri = new ECDsaOpenSsl(priKeyHandle))
{
    byte[] signature = ecdsaPri.SignData(data, HashAlgorithmName.SHA256);
    // Do stuff with signature created by TPM.
}

Hay mejoras en el rendimiento durante el intercambio de claves TLS, así como mejoras en las interacciones con claves privadas RSA que utilizan componentes ENGINE.

Seguridad de CNG de Windows basada en la virtualización

Windows 11 ha agregado nuevas API para ayudar a proteger las claves de Windows con seguridad basada en virtualización (VBS). Con esta nueva funcionalidad, las claves se pueden proteger frente a ataques de robo de claves de nivel de administrador con un efecto insignificante en el rendimiento, la confiabilidad o la escala.

.NET 9 ha agregado marcas coincidentes CngKeyCreationOptions . Se agregaron las tres marcas siguientes:

  • Correspondencia de CngKeyCreationOptions.PreferVbs con NCRYPT_PREFER_VBS_FLAG
  • Correspondencia de CngKeyCreationOptions.RequireVbs con NCRYPT_REQUIRE_VBS_FLAG
  • Correspondencia de CngKeyCreationOptions.UsePerBootKey con NCRYPT_USE_PER_BOOT_KEY_FLAG

En el fragmento de código siguiente se muestra cómo usar una de las marcas:

using System.Security.Cryptography;

CngKeyCreationParameters cngCreationParams = new()
{
    Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider,
    KeyCreationOptions = CngKeyCreationOptions.RequireVbs | CngKeyCreationOptions.OverwriteExistingKey,
};

using (CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256, "myKey", cngCreationParams))
using (ECDsaCng ecdsa = new ECDsaCng(key))
{
    // Do stuff with the key.
}

Fecha y hora: nuevas sobrecargas de TimeSpan.From*

La TimeSpan clase ofrece varios From* métodos que permiten crear un TimeSpan objeto mediante double. Sin embargo, dado que double es un formato de punto flotante basado en binarios, la imprecisión inherente puede provocar errores. Por ejemplo, TimeSpan.FromSeconds(101.832) podría no representar 101 seconds, 832 millisecondsexactamente , sino aproximadamente 101 seconds, 831.9999999999936335370875895023345947265625 milliseconds. Esta discrepancia ha causado confusión frecuente y tampoco es la manera más eficaz de representar estos datos. Para solucionar esto, .NET 9 agrega nuevas sobrecargas que permiten crear TimeSpan objetos a partir de enteros. Hay nuevas sobrecargas de FromDays, FromHours, FromMinutes, FromSeconds, FromMilliseconds y FromMicroseconds.

En el código siguiente se muestra un ejemplo de llamada a double y a una de las nuevas sobrecargas de enteros.

TimeSpan timeSpan1 = TimeSpan.FromSeconds(value: 101.832);
Console.WriteLine($"timeSpan1 = {timeSpan1}");
// timeSpan1 = 00:01:41.8319999

TimeSpan timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832);
Console.WriteLine($"timeSpan2 = {timeSpan2}");
// timeSpan2 = 00:01:41.8320000

Inserción de dependencias: constructor ActivatorUtilities.CreateInstance

La resolución del constructor de ActivatorUtilities.CreateInstance ha cambiado en .NET 9. Anteriormente, es posible que no se llame a un constructor marcado explícitamente mediante el ActivatorUtilitiesConstructorAttribute atributo , en función del orden de los constructores y el número de parámetros de constructor. La lógica ha cambiado en .NET 9 de modo que siempre se llame a un constructor que tenga el atributo .

Diagnósticos

Debug.Assert informa de la condición de aserción de forma predeterminada

Debug.Assert normalmente se utiliza para ayudar a validar las condiciones que se espera siempre sean verdaderas. El error suele indicar un error en el código. Hay muchas sobrecargas de Debug.Assert, la más sencilla de las cuales solo acepta una condición:

Debug.Assert(a > 0 && b > 0);

La afirmación falla si la condición es falsa. Sin embargo, históricamente, dichas afirmaciones carecían de información sobre qué condición falló. A partir de .NET 9, si el usuario no proporciona explícitamente ningún mensaje, la aserción contendrá la representación textual de la condición. Por ejemplo, para el ejemplo de aserción anterior, en lugar de obtener un mensaje como:

Process terminated. Assertion failed.
   at Program.SomeMethod(Int32 a, Int32 b)

El mensaje sería ahora:

Process terminated. Assertion failed.
a > 0 && b > 0
   at Program.SomeMethod(Int32 a, Int32 b)

Anteriormente, solo se podía vincular una Activity de seguimiento a otros contextos de seguimiento cuando se creaba la Activity. Novedad de .NET 9, la AddLink(ActivityLink) API le permite vincular un Activity objeto a otros contextos de seguimiento después de crearlo. Este cambio también se alinea con las especificaciones de OpenTelemetry .

ActivityContext activityContext = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None);
ActivityLink activityLink = new(activityContext);

Activity activity = new("LinkTest");
activity.AddLink(activityLink);

Instrumento Metrics.Gauge

System.Diagnostics.Metrics ahora proporciona la herramienta Gauge<T> según la especificación de OpenTelemetry. El Gauge instrumento está diseñado para registrar valores no aditivos cuando se producen cambios. Por ejemplo, puede medir el nivel de ruido de fondo, donde sumar los valores de varias salas sería no sentido. El Gauge instrumento es un tipo genérico que puede registrar cualquier tipo de valor, como int, doubleo decimal.

En el ejemplo siguiente se muestra el uso del instrumento Gauge.

Meter soundMeter = new("MeasurementLibrary.Sound");
Gauge<int> gauge = soundMeter.CreateGauge<int>(
    name: "NoiseLevel",
    unit: "dB", // Decibels.
    description: "Background Noise Level"
    );
gauge.Record(10, new TagList() { { "Room1", "dB" } });

Escucha de comodines de medidor fuera de proceso

Ya es posible escuchar medidores fuera de proceso mediante el proveedor de origen de eventos System.Diagnostics.Metrics, pero antes de .NET 9, se tenía que indicar el nombre del medidor completo. En .NET 9, puede escuchar todos los medidores mediante el carácter comodín *, que permite capturar las métricas de todos los medidores de un proceso. Además, agrega compatibilidad para escuchar por prefijo de medidor, por lo que puede escuchar todos los medidores cuyos nombres comienzan con un prefijo especificado. Por ejemplo, especificar MyMeter* habilita la escucha de todos los medidores con nombres que comienzan por MyMeter.

// The complete meter name is "MyCompany.MyMeter".
var meter = new Meter("MyCompany.MyMeter");
// Create a counter and allow publishing values.
meter.CreateObservableCounter("MyCounter", () => 1);

// Create the listener to use the wildcard character
// to listen to all meters using prefix names.
MyEventListener listener = new MyEventListener();

La MyEventListener clase se define de la manera siguiente.

internal class MyEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        Console.WriteLine(eventSource.Name);
        if (eventSource.Name == "System.Diagnostics.Metrics")
        {
            // Listen to all meters with names starting with "MyCompany".
            // If using "*", allow listening to all meters.
            EnableEvents(
                eventSource,
                EventLevel.Informational,
                (EventKeywords)0x3,
                new Dictionary<string, string?>() { { "Metrics", "MyCompany*" } }
                );
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        // Ignore other events.
        if (eventData.EventSource.Name != "System.Diagnostics.Metrics" ||
            eventData.EventName == "CollectionStart" ||
            eventData.EventName == "CollectionStop" ||
            eventData.EventName == "InstrumentPublished"
            )
            return;

        Console.WriteLine(eventData.EventName);

        if (eventData.Payload is not null)
        {
            for (int i = 0; i < eventData.Payload.Count; i++)
                Console.WriteLine($"\t{eventData.PayloadNames![i]}: {eventData.Payload[i]}");
        }
    }
}

Al ejecutar el código, la salida es la siguiente:

CounterRateValuePublished
        sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
        meterName: MyCompany.MyMeter
        meterVersion:
        instrumentName: MyCounter
        unit:
        tags:
        rate: 0
        value: 1
        instrumentId: 1
CounterRateValuePublished
        sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
        meterName: MyCompany.MyMeter
        meterVersion:
        instrumentName: MyCounter
        unit:
        tags:
        rate: 0
        value: 1
        instrumentId: 1

También puede usar el carácter comodín para escuchar métricas con herramientas de seguimiento como dotnet-counters.

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 ejemplo siguiente 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 permite implementar flujos de trabajo más de propósito general. En el ejemplo siguiente 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}");
}

Generador de origen de registros

C# 12 introdujo constructores principales, que permiten definir un constructor directamente en la declaración de clase. El generador de origen de registro ahora admite el registro mediante clases que tienen un constructor principal.

public partial class ClassWithPrimaryConstructor(ILogger logger)
{
    [LoggerMessage(0, LogLevel.Debug, "Test.")]
    public partial void Test();
}

Varios

En esta sección, busque información sobre:

allows ref struct usado en bibliotecas

C# 13 presenta la capacidad de restringir un parámetro genérico con allows ref struct, que indica al compilador y el tiempo de ejecución que ref struct se puede usar para ese parámetro genérico. Muchas API que son compatibles con este sistema ya incluyen anotaciones. Por ejemplo, el String.Create método tiene una sobrecarga que permite crear un objeto string escribiendo directamente en su memoria, representada como un span. Este método tiene un argumento TState que se pasa del generador de la llamada al delegado que realiza la escritura real.

Ese TState parámetro de tipo en String.Create ahora está anotado con allows ref struct:

public static string Create<TState>(int length, TState state, SpanAction<char, TState> action)
    where TState : allows ref struct;

Esta anotación le permite pasar un intervalo (o cualquier otro ref struct) como entrada para este método.

En el ejemplo siguiente se ve una nueva sobrecarga String.ToLowerInvariant() que emplea esta funcionalidad.

public static string ToLowerInvariant(ReadOnlySpan<char> input) =>
    string.Create(span.Length, input, static (stringBuffer, input) => span.ToLowerInvariant(stringBuffer));

Expansión SearchValues

.NET 8 introdujo el SearchValues<T> tipo , que proporciona una solución optimizada para buscar conjuntos específicos de caracteres o bytes dentro de intervalos. En .NET 9, SearchValues se ha ampliado para admitir la búsqueda de subcadenas dentro de una cadena mayor.

En el ejemplo siguiente se buscan varios nombres de animales dentro de un valor de cadena y se devuelve un índice al primero encontrado.

private static readonly SearchValues<string> s_animals =
    SearchValues.Create(["cat", "mouse", "dog", "dolphin"], StringComparison.OrdinalIgnoreCase);

public static int IndexOfAnimal(string text) =>
    text.AsSpan().IndexOfAny(s_animals);

Esta nueva funcionalidad tiene una implementación optimizada que aprovecha la compatibilidad con SIMD en la plataforma subyacente. También permite optimizar los tipos de nivel superior. Por ejemplo, Regex ahora utiliza esta funcionalidad como parte de su implementación.

Redes

SocketsHttpHandler es el valor predeterminado en HttpClientFactory

HttpClientFactory crea HttpClient objetos respaldados por HttpClientHandler, de forma predeterminada. HttpClientHandler está respaldado por SocketsHttpHandler, que es mucho más configurable, incluida la administración de la duración de la conexión. HttpClientFactory ahora usa SocketsHttpHandler de forma predeterminada y la configura para establecer límites en sus duraciones de conexión para que coincidan con la de la duración de rotación especificada en la fábrica.

System.Net.ServerSentEvents

Los eventos enviados por el servidor (SSE) son un protocolo sencillo y popular para transmitir datos de un servidor a un cliente. Se usa, por ejemplo, por OpenAI como parte del streaming de texto generado a partir de sus servicios de IA. Para simplificar el consumo de SSE, la nueva System.Net.ServerSentEvents biblioteca proporciona un analizador para ingerir fácilmente eventos enviados por el servidor.

En el código siguiente se muestra el uso de la nueva clase .

Stream responseStream = new MemoryStream();
await foreach (SseItem<string> e in SseParser.Create(responseStream).EnumerateAsync())
{
    Console.WriteLine(e.Data);
}

Reanudación de TLS con certificados de cliente en Linux

La reanudación de TLS es una característica del protocolo TLS que permite reanudar sesiones establecidas previamente en un servidor. Al hacerlo, se evitan algunos ciclos de ida y vuelta y se ahorran recursos computacionales durante el protocolo de enlace de TLS.

La reanudación de TLS ya se ha admitido en Linux para conexiones SslStream sin certificados de cliente. .NET 9 agrega compatibilidad con la reanudación TLS de conexiones TLS autenticadas mutuamente, que son comunes en escenarios de servidor a servidor. La característica se habilita automáticamente.

Tiempo de espera y ping de conexión persistente de WebSocket

Las nuevas API en ClientWebSocketOptions y WebSocketCreationOptions le permiten confirmar el envío de pings de WebSocket y abortar la conexión si el nodo no responde a tiempo.

Hasta ahora, podría especificar un KeepAliveInterval para evitar que la conexión permanezca inactiva, pero no había ningún mecanismo integrado para exigir que el elemento del mismo nivel responda.

En el ejemplo siguiente se hace ping al servidor cada 5 segundos y se anula la conexión si no responde en un segundo.

using var cws = new ClientWebSocket();
cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
cws.Options.KeepAliveInterval = TimeSpan.FromSeconds(5);
cws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1);

await cws.ConnectAsync(uri, httpClient, cancellationToken);

HttpClientFactory ya no registra los valores de encabezado de forma predeterminada

Los eventos de LogLevel.Trace registrados por HttpClientFactory ya no incluyen valores de encabezado de forma predeterminada. Puede optar por registrar valores para encabezados específicos a través del RedactLoggedHeaders método auxiliar.

En el ejemplo siguiente se redactan todos los encabezados, excepto para el agente de usuario.

services.AddHttpClient("myClient")
    .RedactLoggedHeaders(name => name != "User-Agent");

Para obtener más información, consulte Los registros de HttpClientFactory eliminan los valores de los encabezados de forma predeterminada.

Reflexión

Ensamblados persistentes

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 bloqueador para los clientes que migran de .NET Framework a .NET. .NET 9 agrega un nuevo tipo, PersistedAssemblyBuilder, que puede usar para guardar un ensamblado emitido.

Para crear una PersistedAssemblyBuilder instancia, llame a su constructor y pase el nombre del ensamblado, el ensamblado principal, System.Private.CoreLib, para hacer referencia a los tipos de tiempo de ejecución base y los atributos personalizados opcionales. Después de emitir todos los miembros al ensamblado, llame al método PersistedAssemblyBuilder.Save(String) para crear un ensamblado con la configuración predeterminada. Si desea establecer el punto de entrada u otras opciones, puede llamar PersistedAssemblyBuilder.GenerateMetadata a y usar los metadatos que devuelve para guardar el ensamblado. El código siguiente muestra un ejemplo de creación de un ensamblado persistente y el establecimiento del punto de entrada.

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

    MethodBuilder entryPoint = tb.DefineMethod(
        "Main",
        MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static
        );
    ILGenerator il = entryPoint.GetILGenerator();
    // ...
    il.Emit(OpCodes.Ret);

    tb.CreateType();

    MetadataBuilder metadataBuilder = ab.GenerateMetadata(
        out BlobBuilder ilStream,
        out BlobBuilder fieldData
        );
    PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(
                    imageCharacteristics: Characteristics.ExecutableImage);

    ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                    header: peHeaderBuilder,
                    metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
                    ilStream: ilStream,
                    mappedFieldData: fieldData,
                    entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken)
                    );

    BlobBuilder peBlob = new BlobBuilder();
    peBuilder.Serialize(peBlob);

    using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
    peBlob.WriteContentTo(fileStream);
}

public static 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]));
}

La nueva PersistedAssemblyBuilder clase incluye compatibilidad con PDB. Puede emitir información de símbolos y usarla para depurar un ensamblado generado. La API tiene una forma similar a la implementación de .NET Framework. Para obtener más información, consulte Emisión de símbolos y generación de PDB.

Análisis de nombres de tipo

TypeName es un analizador para los nombres de tipo ECMA-335 que proporciona casi la misma funcionalidad que System.Type, pero está desacoplado del entorno de tiempo de ejecución. Los componentes como serializadores y compiladores deben analizar y procesar nombres de tipo. Por ejemplo, el compilador AOT nativo ha cambiado al uso de TypeName.

La nueva TypeName clase proporciona:

  • Métodos estáticos Parse y TryParse para analizar la entrada representada como ReadOnlySpan<char>. Ambos métodos aceptan una instancia de clase (un contenedor de TypeNameParseOptions opciones) que permite personalizar el análisis.

  • Name, FullNamey AssemblyQualifiedName propiedades que funcionan exactamente igual que sus homólogos en System.Type.

  • Varias propiedades y métodos que proporcionan información adicional sobre el propio nombre:

    • IsArray, IsSZArray (SZ significa matriz de una sola dimensión, sin indexación cero), IsVariableBoundArrayTypey GetArrayRank para trabajar con matrices.
    • IsConstructedGenericType, GetGenericTypeDefinitiony GetGenericArguments para trabajar con nombres de tipo genéricos.
    • IsByRef y IsPointer para utilizar punteros y referencias administradas.
    • GetElementType() para trabajar con punteros, referencias y matrices.
    • IsNested y DeclaringType para utilizar tipos anidados.
    • AssemblyName, que expone la información del nombre del ensamblado a través de la nueva AssemblyNameInfo clase. A diferencia de AssemblyName, el nuevo tipo es inmutable y el análisis de nombres de culturas no crea instancias de CultureInfo.

Tanto TypeName como AssemblyNameInfo son tipos inmutables y no proporcionan una manera de comprobar la igualdad (no implementan IEquatable). Comparar nombres de ensamblado es sencillo, pero diferentes escenarios deben comparar solo un subconjunto de información expuesta (Name, Version, CultureNamey PublicKeyOrToken).

En el fragmento de código siguiente se muestra un ejemplo de uso.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;

internal class RestrictedSerializationBinder
{
    Dictionary<string, Type> AllowList { get; set; }

    RestrictedSerializationBinder(Type[] allowedTypes)
        => AllowList = allowedTypes.ToDictionary(type => type.FullName!);

    Type? GetType(ReadOnlySpan<char> untrustedInput)
    {
        if (!TypeName.TryParse(untrustedInput, out TypeName? parsed))
        {
            throw new InvalidOperationException($"Invalid type name: '{untrustedInput.ToString()}'");
        }

        if (AllowList.TryGetValue(parsed.FullName, out Type? type))
        {
            return type;
        }
        else if (parsed.IsSimple // It's not generic, pointer, reference, or an array.
            && parsed.AssemblyName is not null
            && parsed.AssemblyName.Name == "MyTrustedAssembly"
            )
        {
            return Type.GetType(parsed.AssemblyQualifiedName, throwOnError: true);
        }

        throw new InvalidOperationException($"Not allowed: '{untrustedInput.ToString()}'");
    }
}

Las nuevas API están disponibles en el System.Reflection.Metadata paquete NuGet, que se pueden usar con versiones de .NET de nivel anterior.

Expresiones regulares

[GeneratedRegex] en propiedades

.NET 7 introdujo el Regex generador de origen y el atributo correspondiente GeneratedRegexAttribute .

El siguiente método parcial se generará automáticamente con todo el código necesario para implementar este Regex.

[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWord();

C# 13 admite propiedades parciales además de métodos parciales, por lo que a partir de .NET 9 también puede usar [GeneratedRegex(...)] en una propiedad .

La siguiente propiedad parcial es el equivalente de la propiedad del ejemplo anterior.

[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWordProperty { get; }

Regex.EnumerateSplits

La Regex clase proporciona un Split método, similar en concepto al String.Split método . Con String.Split, proporcionas uno o más separadores char o string, y la implementación divide el texto de entrada usando esos separadores. Con Regex.Split, en lugar de especificar el separador como char o string, se especifica como un patrón de expresión regular.

En el ejemplo siguiente se muestra Regex.Split.

foreach (string s in Regex.Split("Hello, world! How are you?", "[aeiou]"))
{
    Console.WriteLine($"Split: \"{s}\"");
}

// Output, split by all English vowels:
// Split: "H"
// Split: "ll"
// Split: ", w"
// Split: "rld! H"
// Split: "w "
// Split: "r"
// Split: " y"
// Split: ""
// Split: "?"

Sin embargo, Regex.Split solo acepta una string como entrada y no admite la entrada proporcionada como ReadOnlySpan<char>. Además, genera el conjunto completo de divisiones como string[], lo que requiere asignar una matriz string para contener los resultados y un string para cada división. En .NET 9, el nuevo EnumerateSplits método permite realizar la misma operación, pero con una entrada basada en intervalos y sin incurrir en ninguna asignación para los resultados. Acepta un ReadOnlySpan<char> y devuelve un enumerable de objetos Range que representan los resultados.

En el ejemplo siguiente se muestra Regex.EnumerateSplits, tomando ReadOnlySpan<char> como entrada.

ReadOnlySpan<char> input = "Hello, world! How are you?";
foreach (Range r in Regex.EnumerateSplits(input, "[aeiou]"))
{
    Console.WriteLine($"Split: \"{input[r]}\"");
}

Serialización (System.Text.Json)

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 singleton

Si desea realizar la serialización con las opciones por defecto que ASP.NET Core emplea para las aplicaciones web, use el nuevo JsonSerializerOptions.Web singleton.

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

JsonSchemaExporter

JSON se usa con frecuencia para representar tipos en firmas de método como parte de esquemas de llamada a procedimientos remotos. Se usa, por ejemplo, como parte de las especificaciones de OpenAPI o como parte de las llamadas de herramientas con servicios de INTELIGENCIA ARTIFICIAL como los de OpenAI. Los desarrolladores pueden serializar y deserializar tipos de .NET como JSON mediante System.Text.Json. Pero también deben poder obtener un esquema JSON que describa la forma del tipo .NET (es decir, describe la forma de lo que se serializaría y lo que se puede deserializar). System.Text.Json ahora proporciona el JsonSchemaExporter tipo , que admite la generación de un esquema JSON que representa un tipo de .NET.

Para más información, consulte Exportador de esquemas JSON.

Respetar las anotaciones que aceptan valores NULL

System.Text.Json ahora reconoce las anotaciones de nulabilidad de las propiedades y se puede configurar para hacerlas cumplir durante la serialización y deserialización mediante la bandera RespectNullableAnnotations.

En el código siguiente se muestra cómo establecer la opción:

public static void RunIt()
{
    JsonSerializerOptions options = new() { RespectNullableAnnotations = true };

    // Throws exception: System.Text.Json.JsonException: The property or field
    // 'Title' on type 'Serialization+Book' doesn't allow getting null values.
    // Consider updating its nullability annotation.
    JsonSerializer.Serialize(new Book { Title = null! }, options);

    // Throws exception: System.Text.Json.JsonException: The property or field
    // 'Title' on type 'Serialization+Book' doesn't allow setting null values.
    // Consider updating its nullability annotation.
    JsonSerializer.Deserialize<Book>("""{ "Title" : null }""", options);
}

public class Book
{
    public required string Title { get; set; }
    public string? Author { get; set; }
    public int PublishYear { get; set; }
}

Para obtener más información, consulte Respetar anotaciones con valores null.

Requerir parámetros de constructor no opcionales

Históricamente, System.Text.Json ha tratado los parámetros de constructor no opcionales como opcionales al usar la deserialización basada en constructores. Puede cambiar ese comportamiento mediante la nueva RespectRequiredConstructorParameters marca.

En el código siguiente se muestra cómo establecer la opción:

JsonSerializerOptions options = new() { RespectRequiredConstructorParameters = true };

// Throws exception: System.Text.Json.JsonException: JSON deserialization
// for type 'Serialization+MyPoco' was missing required properties including: 'Value'.
JsonSerializer.Deserialize<MyPoco>("""{}""", options);

El tipo MyPoco se define de la siguiente manera:

record MyPoco(string Value);

Para obtener más información, consulte Parámetros de constructor no opcionales.

Ordenar las propiedades de JsonObject

El JsonObject tipo expone ahora las API ordenadas de tipo diccionario que habilitan la manipulación explícita del orden de propiedades.

JsonObject jObj = new()
{
    ["key1"] = true,
    ["key3"] = 3
};

Console.WriteLine(jObj is IList<KeyValuePair<string, JsonNode?>>); // True.

// Insert a new key-value pair at the correct position.
int key3Pos = jObj.IndexOf("key3") is int i and >= 0 ? i : 0;
jObj.Insert(key3Pos, "key2", "two");

foreach (KeyValuePair<string, JsonNode?> item in jObj)
{
    Console.WriteLine($"{item.Key}: {item.Value}");
}

// Output:
// key1: true
// key2: two
// key3: 3

Para obtener más información, vea Manipular el orden de las propiedades.

Personalización de nombres de miembros de enumeración

El nuevo System.Text.Json.Serialization.JsonStringEnumMemberNameAttribute atributo se puede usar para personalizar los nombres de miembros de enumeración individuales para los tipos que se serializan como cadenas:

JsonSerializer.Serialize(MyEnum.Value1 | MyEnum.Value2); // "Value1, Custom enum value"

[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
enum MyEnum
{
    Value1 = 1,
    [JsonStringEnumMemberName("Custom enum value")]
    Value2 = 2,
}

Para obtener más información, consulte Nombres de miembro de enumeración personalizados.

Transmisión de varios documentos JSON

System.Text.Json.Utf8JsonReader ahora admite la lectura de varios documentos JSON separados por espacios en blanco desde un único búfer o secuencia. De forma predeterminada, el lector produce una excepción si detecta caracteres que no sean de espacio en blanco que estén al final del primer documento de nivel superior. Puede cambiar este comportamiento mediante la AllowMultipleValues marca .

Para más información, consulte Lectura de varios documentos JSON.

Intervalos

En el código de alto rendimiento, los intervalos se suelen usar para evitar asignar cadenas innecesariamente. Span<T> y ReadOnlySpan<T> siguen revolucionando cómo se escribe código en .NET y cada versión se agregan más y más métodos que funcionan en intervalos. .NET 9 incluye las siguientes actualizaciones relacionadas con los intervalos:

Asistentes de archivos

La File clase ahora tiene nuevos asistentes para escribir fácil y directamente ReadOnlySpan<char>/ReadOnlySpan<byte> y ReadOnlyMemory<char>/ReadOnlyMemory<byte> en archivos.

El siguiente código escribe eficazmente un ReadOnlySpan<char> en un archivo.

ReadOnlySpan<char> text = ...;
File.WriteAllText(filePath, text);

También se han agregado nuevos métodos StartsWith<T>(ReadOnlySpan<T>, T) y EndsWith<T>(ReadOnlySpan<T>, T) de extensión para segmentos, lo que facilita comprobar si un ReadOnlySpan<T> empieza o termina con un valor específico T.

El código siguiente usa estas nuevas API de conveniencia.

ReadOnlySpan<char> text = "some arbitrary text";
return text.StartsWith('"') && text.EndsWith('"'); // false

Sobrecargas params ReadOnlySpan<T>

C# siempre ha admitido la marcación de parámetros de matriz como params. Esta palabra clave habilita una sintaxis de llamada simplificada. Por ejemplo, el String.Join(String, String[]) segundo parámetro del método está marcado con params. Puede llamar a esta sobrecarga con una matriz o pasando los valores uno a uno:

string result = string.Join(", ", new string[3] { "a", "b", "c" });
string result = string.Join(", ", "a", "b", "c");

Antes de .NET 9, cuando se pasan los valores individualmente, el compilador de C# emite código idéntico a la primera llamada mediante la generación de una matriz implícita alrededor de los tres argumentos.

A partir de C# 13, puede usar params con cualquier argumento que se pueda construir a través de una expresión de colección, incluidos los intervalos (Span<T> y ReadOnlySpan<T>). Esto es beneficioso para la facilidad de uso y el rendimiento. El compilador de C# puede almacenar los argumentos en la pila, envolverlos en un span y pasarlos al método, lo que evita la asignación implícita de un array que de otro modo habría resultado.

.NET 9 incluye más de 60 métodos con un params ReadOnlySpan<T> parámetro . Algunas son nuevas sobrecargas y algunas son métodos existentes que ya aceptaban un ReadOnlySpan<T>, pero ahora tienen ese mismo parámetro marcado con params. El efecto neto es si actualiza a .NET 9 y vuelve a compilar el código, verá mejoras de rendimiento sin realizar cambios en el código. Esto se debe a que el compilador prefiere enlazar a sobrecargas basadas en intervalos que a las sobrecargas basadas en matrices.

Por ejemplo, String.Join ahora incluye la siguiente sobrecarga, que implementa el nuevo patrón: String.Join(String, ReadOnlySpan<String>)

Ahora, se realiza una llamada como string.Join(", ", "a", "b", "c") sin asignar una matriz para pasar los argumentos "a", "b" y "c".

Enumerar mediante segmentos ReadOnlySpan<char>.Split()

string.Split es un método práctico para crear particiones rápidas de una cadena con uno o varios separadores proporcionados. Sin embargo, para el código centrado en el rendimiento, el perfil de asignación de string.Split puede ser prohibitivo, ya que asigna una cadena para cada componente analizado y un string[] para almacenarlos todos. Tampoco funciona con intervalos, por lo que si tiene un ReadOnlySpan<char>, se verá obligado a asignar otra cadena al convertirla en una cadena para poder llamar a string.Split en ella.

En .NET 8, se introdujo un conjunto de métodos de Split y SplitAny para ReadOnlySpan<char>. En lugar de devolver un nuevo string[], estos métodos aceptan un destino Span<Range> en el que se escriben los índices de límite de cada componente. Esto hace que la operación sea totalmente libre de asignación. Estos métodos son adecuados para usar cuando el número de intervalos es conocido y pequeño.

En .NET 9, se han agregado nuevas sobrecargas de Split y SplitAny para permitir el análisis incremental de un ReadOnlySpan<T> con un número de segmentos desconocido a priori. Los nuevos métodos permiten enumerar cada segmento, que se representa de forma similar como un Range que se puede usar para dividir el intervalo original.

public static bool ListContainsItem(ReadOnlySpan<char> span, string item)
{
    foreach (Range segment in span.Split(','))
    {
        if (span[segment].SequenceEquals(item))
        {
            return true;
        }
    }

    return false;
}

System.Formats

La posición o desplazamiento de los datos en el flujo contenedor de un objeto TarEntry es ahora una propiedad pública. TarEntry.DataOffset devuelve la posición en el flujo de archivo de la entrada donde se encuentra el primer byte de datos de la entrada. Los datos de la entrada se encapsulan en un subflujo al que se puede acceder a través de TarEntry.DataStream, que oculta la posición real de los datos con respecto a la secuencia del archivo. Eso es suficiente para la mayoría de los usuarios, pero si necesita más flexibilidad y desea conocer la posición inicial real de los datos en la secuencia de archivo, la nueva TarEntry.DataOffset API facilita la compatibilidad con características como el acceso simultáneo con archivos TAR muy grandes.

// Create stream for tar ball data in Azure Blob Storage.
BlobClient blobClient = new(connectionString, blobContainerName, blobName);
Stream blobClientStream = await blobClient.OpenReadAsync(options, cancellationToken);

// Create TarReader for the stream and get a TarEntry.
TarReader tarReader = new(blobClientStream);
System.Formats.Tar.TarEntry? tarEntry = await tarReader.GetNextEntryAsync();

if (tarEntry is null)
    return;

// Get position of TarEntry data in blob stream.
long entryOffsetInBlobStream = tarEntry.DataOffset;
long entryLength = tarEntry.Length;

// Create a separate stream.
Stream newBlobClientStream = await blobClient.OpenReadAsync(options, cancellationToken);
newBlobClientStream.Seek(entryOffsetInBlobStream, SeekOrigin.Begin);

// Read tar ball content from separate BlobClient stream.
byte[] bytes = new byte[entryLength];
await newBlobClientStream.ReadExactlyAsync(bytes, 0, (int)entryLength);

System.Guid

NewGuid() crea un Guid relleno principalmente con datos aleatorios criptográficamente seguros, siguiendo la especificación UUID Versión 4 en RFC 9562. Este mismo RFC también define otras versiones, como la versión 7, que "presenta un campo de valor ordenado por tiempo derivado del sistema de medida de tiempo implementado y conocido en todo el mundo". En otras palabras, gran parte de los datos sigue siendo aleatorio, pero algunos de ellos están reservados para los datos basados en una marca de tiempo, lo que permite que estos valores tengan un criterio de ordenación natural. En .NET 9, puede crear un Guid según Version 7 a través de los nuevos métodos Guid.CreateVersion7() y Guid.CreateVersion7(DateTimeOffset). También puede usar la nueva Version propiedad para recuperar el campo de versión de un Guid objeto.

System.IO

Compresión con zlib-ng

System.IO.Compression características como ZipArchive, DeflateStream, GZipStreamy ZLibStream se basan principalmente en la biblioteca zlib. A partir de .NET 9, estas características usan en su lugar zlib-ng, una biblioteca que produce un procesamiento más coherente y eficaz en una matriz más amplia de sistemas operativos y hardware.

Opciones de compresión ZLib y Brotli

ZLibCompressionOptions y BrotliCompressionOptions son nuevos tipos para establecer el nivel de compresión y la estrategia específicos del algoritmo (Default, Filtered, HuffmanOnly, RunLengthEncodingo Fixed). Estos tipos están dirigidos a los usuarios que desean una configuración más ajustada que la única opción existente, <System.IO.Compression.CompressionLevel>.

Los nuevos tipos de opciones de compresión se pueden expandir en el futuro.

En el fragmento de código siguiente se muestra un ejemplo de uso:

private MemoryStream CompressStream(Stream uncompressedStream)
{
    MemoryStream compressorOutput = new();
    using ZLibStream compressionStream = new(
        compressorOutput,
        new ZLibCompressionOptions()
        {
            CompressionLevel = 6,
            CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly
        }
        );
    uncompressedStream.CopyTo(compressionStream);
    compressionStream.Flush();

    return compressorOutput;
}

Documentos XPS de la impresora virtual XPS

Los documentos XPS procedentes de una impresora virtual V4 XPS anteriormente no se podían abrir con la System.IO.Packaging biblioteca, debido a la falta de compatibilidad con el control de archivos .piece . Esta brecha se ha solucionado en .NET 9.

System.Numerics

Límite superior de BigInteger

BigInteger admite la representación de valores enteros de longitud esencialmente arbitraria. Sin embargo, en la práctica, la longitud está restringida por límites del equipo subyacente, como la memoria disponible o el tiempo que tardaría en calcular una expresión determinada. Además, existen algunas API que producen un error en las entradas dadas que dan como resultado un valor demasiado grande. Debido a estos límites, .NET 9 aplica una longitud máxima de BigInteger, que es que no puede contener más de (2^31) - 1 (aproximadamente 2,14 mil millones de bits). Este número representa una asignación de casi 256 MB y contiene aproximadamente 646,5 millones de dígitos. Este nuevo límite garantiza que todas las API expuestas se comporten bien y sean coherentes, a la vez que permiten números que están mucho más allá de la mayoría de los escenarios de uso.

API BigMul

BigMul es una operación que genera el producto completo de dos números. .NET 9 añade API BigMul específicas en int, long, uint y ulong cuyo tipo de valor devuelto es el siguiente tipo entero mayor que los tipos de parámetro.

Las nuevas API son:

API de conversión de vectores

.NET 9 agrega API de extensión dedicadas para convertir entre Vector2, Vector3, Vector4, Quaterniony Plane.

Las nuevas API son las siguientes:

En el caso de las conversiones de mismo tamaño, como entre Vector4, Quaterniony Plane, estas conversiones son de costo cero. Se puede decir lo mismo para restringir las conversiones, como de Vector4 a Vector2 o Vector3. Para las conversiones de ampliación, como de Vector2 o Vector3 a Vector4, existe una API normal, que inicializa los nuevos elementos a 0 y una API con sufijo Unsafe que deja estos nuevos elementos sin definir y, por tanto, puede ser sin costo.

API de creación de vectores

Hay nuevas API Create expuestas para Vector, Vector2, Vector3 y Vector4 que tienen paridad con las API equivalentes expuestas para los tipos de vectores de hardware en el espacio de nombres System.Runtime.Intrinsics.

Para obtener más información sobre las nuevas API, consulte:

Estas API sirven principalmente para que sean más prácticas y tengan mayor uniformidad con los tipos acelerados por SIMD de .NET.

Aceleración adicional

Se han realizado mejoras adicionales de rendimiento en muchos tipos del espacio de nombres System.Numerics, incluidos BigInteger, Vector2, Vector3, Vector4, Quaternion, y Plane.

En algunos casos, esto ha dado lugar a un aumento de velocidad de 2 a 5 veces en las API principales, incluida la multiplicación de Matrix4x4, la creación de Plane a partir de una serie de vértices, la concatenación de Quaternion y la computación del producto cruzado de un Vector3.

También hay funciones de plegado constante para la API SinCos, que calcula tanto Sin(x) como Cos(x) en una sola llamada, haciéndolo más eficiente.

Tensores para IA

Los tensores son la estructura de datos de piedra angular de la inteligencia artificial (IA). A menudo se pueden considerar como matrices multidimensionales.

Los tensores se usan para:

  • Represente y codifique datos como secuencias de texto (tokens), imágenes, vídeo y audio.
  • Manipular de forma eficaz los datos dimensionales superiores.
  • Aplique cálculos de forma eficaz en datos dimensionales superiores.
  • Almacenar información de peso y cálculos intermedios (en redes neuronales).

Para usar las API de tensor de .NET, instale el paquete NuGet System.Numerics.Tensors .

Nuevo tipo Tensor<T>

El nuevo Tensor<T> tipo expande las funcionalidades de inteligencia artificial de las bibliotecas y el entorno de ejecución de .NET. Este tipo:

  • Proporciona una interoperabilidad eficaz con bibliotecas de INTELIGENCIA ARTIFICIAL como ML.NET, TorchSharp y ONNX Runtime con cero copias siempre que sea posible.
  • Se basa en TensorPrimitives para realizar operaciones matemáticas eficaces.
  • Permite una manipulación de datos fácil y eficaz al proporcionar operaciones de indexación y segmentación.
  • No es un reemplazo de las bibliotecas existentes de inteligencia artificial y aprendizaje automático. En su lugar, está pensado para proporcionar un conjunto común de API para reducir la duplicación y las dependencias de código, y para lograr un mejor rendimiento mediante las características más recientes del entorno de ejecución.

En los códigos siguientes se muestran algunas de las API incluidas con el nuevo Tensor<T> tipo.

// Create a tensor (1 x 3).
Tensor<int> t0 = Tensor.Create([1, 2, 3], [1, 3]); // [[1, 2, 3]]

// Reshape tensor (3 x 1).
Tensor<int> t1 = t0.Reshape(3, 1); // [[1], [2], [3]]

// Slice tensor (2 x 1).
Tensor<int> t2 = t1.Slice(1.., ..); // [[2], [3]]

// Broadcast tensor (3 x 1) -> (3 x 3).
// [
//  [ 1, 1, 1],
//  [ 2, 2, 2],
//  [ 3, 3, 3]
// ]
var t3 = Tensor.Broadcast<int>(t1, [3, 3]);

// Math operations.
var t4 = Tensor.Add(t0, 1); // [[2, 3, 4]]
var t5 = Tensor.Add(t0.AsReadOnlyTensorSpan(), t0); // [[2, 4, 6]]
var t6 = Tensor.Subtract(t0, 1); // [[0, 1, 2]]
var t7 = Tensor.Subtract(t0.AsReadOnlyTensorSpan(), t0); // [[0, 0, 0]]
var t8 = Tensor.Multiply(t0, 2); // [[2, 4, 6]]
var t9 = Tensor.Multiply(t0.AsReadOnlyTensorSpan(), t0); // [[1, 4, 9]]
var t10 = Tensor.Divide(t0, 2); // [[0.5, 1, 1.5]]
var t11 = Tensor.Divide(t0.AsReadOnlyTensorSpan(), t0); // [[1, 1, 1]]

Nota:

Esta API está marcada como experimental para .NET 9.

TensorPrimitives

La System.Numerics.Tensors biblioteca incluye la TensorPrimitives clase , que proporciona métodos estáticos para realizar operaciones numéricas en intervalos de valores. En .NET 9, el ámbito de los métodos expuestos por TensorPrimitives se ha ampliado significativamente, aumentando de 40 (en .NET 8) a casi 200 sobrecargas. El área expuesta abarca operaciones numéricas conocidas de tipos como Math y MathF. También incluye las interfaces matemáticas genéricas como INumber<TSelf>, excepto en lugar de procesar un valor individual, procesan un intervalo de valores. Muchas operaciones también se han acelerado a través de implementaciones optimizadas para SIMD para .NET 9.

TensorPrimitives ahora expone sobrecargas genéricas para cualquier tipo T que implemente una interfaz determinada. (La versión de .NET 8 solo incluía sobrecargas para manipular intervalos de valores float). Por ejemplo, la nueva sobrecarga CosineSimilarity<T>(ReadOnlySpan<T>, ReadOnlySpan<T>) calcula la similitud de cosenos en dos vectores de valores float, double o Half, o bien, de valores de cualquier otro tipo que implemente IRootFunctions<TSelf>.

Compare la precisión de la operación de similitud coseno en dos vectores de tipo float frente a double:

ReadOnlySpan<float> vector1 = [1, 2, 3];
ReadOnlySpan<float> vector2 = [4, 5, 6];
Console.WriteLine(TensorPrimitives.CosineSimilarity(vector1, vector2));
// Prints 0.9746318

ReadOnlySpan<double> vector3 = [1, 2, 3];
ReadOnlySpan<double> vector4 = [4, 5, 6];
Console.WriteLine(TensorPrimitives.CosineSimilarity(vector3, vector4));
// Prints 0.9746318461970762

Subprocesos

Las API de subprocesos incluyen mejoras para iterar diferentes tareas, para canales con prioridad, que pueden ordenar los elementos en lugar de usar el método "el primero en entrar es el primero en salir" (FIFO), así como Interlocked.CompareExchange para admitir más tipos.

Task.WhenEach

Se han agregado varias API nuevas útiles para trabajar con Task<TResult> objetos. El nuevo método Task.WhenEach permite iterar las tareas a medida que se completan mediante una declaración await foreach. Ya no es necesario llevar a cabo acciones como llamar repetidamente a Task.WaitAny en un conjunto de tareas para seleccionar la siguiente que se complete.

El siguiente código hace múltiples llamadas HttpClient y opera sobre sus resultados a medida que estos se completan.

using HttpClient http = new();

Task<string> dotnet = http.GetStringAsync("http://dot.net");
Task<string> bing = http.GetStringAsync("http://www.bing.com");
Task<string> ms = http.GetStringAsync("http://microsoft.com");

await foreach (Task<string> t in Task.WhenEach(bing, dotnet, ms))
{
    Console.WriteLine(t.Result);
}

Canal prioritizado ilimitado

El System.Threading.Channels espacio de nombres permite crear canales de primero en entrar, primero en salir (FIFO) mediante los métodos CreateBounded y CreateUnbounded. Con los canales FIFO, los elementos se leen desde el canal en el orden en que se escribieron en él. En .NET 9, se ha agregado el nuevo método CreateUnboundedPrioritized, que ordena los elementos de forma que el siguiente elemento leído del canal sea el más importante, según Comparer<T>.Default o un IComparer<T> personalizado.

En el ejemplo siguiente se usa el nuevo método para crear un canal que genere los números del 1 al 5 en orden, aunque se escriban en el canal en un orden diferente.

Channel<int> c = Channel.CreateUnboundedPrioritized<int>();

await c.Writer.WriteAsync(1);
await c.Writer.WriteAsync(5);
await c.Writer.WriteAsync(2);
await c.Writer.WriteAsync(4);
await c.Writer.WriteAsync(3);
c.Writer.Complete();

while (await c.Reader.WaitToReadAsync())
{
    while (c.Reader.TryRead(out int item))
    {
        Console.Write($"{item} ");
    }
}

// Output: 1 2 3 4 5

Interlocked.CompareExchange para usar más tipos

En versiones anteriores de .NET, Interlocked.Exchange y Interlocked.CompareExchange tenían sobrecargas para trabajar con int, uint, long, ulong, nint, nuint, float, double y object, así como una sobrecarga genérica para trabajar con cualquier tipo de referencia T. En .NET 9, hay nuevas sobrecargas para trabajar atómicamente con byte, sbyte, short y ushort. Además, se ha quitado la restricción genérica de las sobrecargas genéricas Interlocked.Exchange<T> y Interlocked.CompareExchange<T>, por lo que esos métodos ya no están restringidos para funcionar únicamente con tipos de referencia. Ahora pueden trabajar con cualquier tipo primitivo, que incluye todos los tipos mencionados anteriormente, así como bool y char, así como cualquier enum tipo.