Novedades en el tiempo de ejecución de .NET 8

En este artículo se describen las nuevas características del tiempo de ejecución de .NET para .NET 8.

Mejoras en el rendimiento

.NET 8 incluye mejoras en la generación de código y la compilación Just-In-Time (JIT):

  • Mejoras de rendimiento de Arm64
  • Mejoras de SIMD
  • Compatibilidad con extensiones ISA AVX-512 (consulte Vector512 y AVX-512)
  • Mejoras nativas de la nube
  • Mejoras de rendimiento de JIT
  • Optimizaciones generales y de bucles
  • Acceso optimizado para los campos marcados con ThreadStaticAttribute
  • Asignación consecutiva de registros. Arm64 tiene dos instrucciones para la búsqueda de vectores de tabla, que requieren que todas las entidades de sus operandos de tupla estén presentes en registros consecutivos.
  • JIT/NativeAOT ahora puede expandir y vectorizar automáticamente algunas operaciones de memoria con SIMD, como comparación, copia y puesta a cero, si puede determinar sus tamaños en tiempo de compilación.

Además, se ha mejorado la Optimización guiada por perfiles (PGO) dinámica y ahora está habilitada de forma predeterminada. Ya no es necesario usar una opción de configuración en tiempo de ejecución para habilitarla. La PGO dinámica funciona de la mano con la compilación en niveles para optimizar aún más el código en función de la instrumentación adicional que se pone en marcha durante el nivel 0.

De media, la PGO dinámica aumenta el rendimiento en un 15 %. En un conjunto de referencia de aproximadamente 4600 pruebas, un 23 % constató mejoras de rendimiento del 20 % o más.

Promoción de estructura de Codegen

.NET 8 incluye un nuevo pase de optimización de promoción física para codegen que generaliza la capacidad de JIT para promover variables de estructura. Esta optimización (también denominada reemplazo escalar de agregados) reemplaza los campos de las variables de estructura por variables primitivas que el JIT puede razonar y optimizar más precisamente.

El JIT ya admite esta optimización, pero con varias limitaciones grandes, entre las que se incluyen:

  • Solo se admite para estructuras con cuatro o menos campos.
  • Solo se admitía si cada campo era un tipo primitivo o una estructura simple que encapsulaba un tipo primitivo.

La promoción física elimina estas limitaciones, lo que corrige una serie de problemas JIT de larga duración.

Recolección de elementos no utilizados

.NET 8 agrega una funcionalidad para ajustar el límite de memoria sobre la marcha. Esto es útil en escenarios de servicio en la nube, donde la demanda es variable. Para ser rentable, los servicios deben escalar y reducir verticalmente el consumo de recursos a medida que fluctúa la demanda. Cuando un servicio detecta una disminución de la demanda, puede reducir verticalmente el consumo de recursos reduciendo su límite de memoria. Anteriormente, esto produciría un error porque el recolector de elementos no utilizados (GC) no era consciente del cambio y podría asignar más memoria que el nuevo límite. Con este cambio, puede llamar a la API RefreshMemoryLimit() para actualizar el GC con el nuevo límite de memoria.

Existen algunas limitaciones a tener en cuenta, por ejemplo:

  • En plataformas de 32 bits (por ejemplo, Windows x86 y Linux ARM), .NET no puede establecer un nuevo límite máximo de montón si aún no hay uno.
  • La API podría devolver un código de estado distinto de cero que indica que se produjo un error en la actualización. Esto puede ocurrir si la reducción vertical es demasiado agresiva y no deja espacio para que el GC funcione correctamente. En este caso, considere la posibilidad de llamar a GC.Collect(2, GCCollectionMode.Aggressive) para reducir el uso de memoria actual e inténtelo de nuevo.
  • Si escala verticalmente el límite de memoria más allá del tamaño que el GC cree que el proceso puede controlar durante el inicio, la llamada RefreshMemoryLimit se realizará correctamente, pero no podrá usar más memoria de lo que percibe como límite.

El siguiente fragmento de código muestra cómo llamar a la API.

GC.RefreshMemoryLimit();

También puede actualizar algunas de las opciones de configuración de GC relacionadas con el límite de memoria. El siguiente fragmento de código establece el límite máximo del montón en 100 mebibytes (MiB):

AppContext.SetData("GCHeapHardLimit", (ulong)100 * 1_024 * 1_024);
GC.RefreshMemoryLimit();

La API puede producir un InvalidOperationException si el límite máximo no es válido, por ejemplo, en el caso de porcentajes de límite máximo de montón negativos y si el límite máximo es demasiado bajo. Esto puede ocurrir si el límite máximo del montón que establecerá la actualización, ya sea debido a la nueva configuración de AppData o implícita por los cambios de límite de memoria del contenedor, es menor que lo que ya se ha confirmado.

Globalización para aplicaciones móviles

Las aplicaciones móviles en iOS, tvOS y MacCatalyst pueden optar por un nuevo modo de globalización híbrido que utiliza un paquete ICU más ligero. En el modo híbrido, los datos de globalización se extraen en parte del paquete ICU y en parte de las llamadas a las API nativas. El modo híbrido sirve para todas las configuraciones regionales compatibles con móviles.

El modo híbrido es el más adecuado para las aplicaciones que no pueden funcionar en el modo de globalización invariante y que utilizan cultivos recortados de los datos de la ICU en el móvil. También puede usarlo cuando quiera cargar un archivo de datos de ICU más pequeño. (El archivo icudt_hybrid.dat es un 34,5 % más pequeño que el archivo de datos de ICU predeterminado icudt.dat).

Para utilizar el modo de globalización híbrido, establezca la HybridGlobalization propiedad MSBuild en true:

<PropertyGroup>
  <HybridGlobalization>true</HybridGlobalization>
</PropertyGroup>

Existen algunas limitaciones a tener en cuenta:

  • Debido a las limitaciones de la API nativa, no todas las API de globalización se admiten en modo híbrido.
  • Algunas de las API admitidas tienen un comportamiento diferente.

Para comprobar si su aplicación se ve afectada, consulte Diferencias de comportamiento.

Interoperabilidad COM generada por el origen

.NET 8 incluye un nuevo generador de origen que admite la interoperación con interfaces COM. Puede usar GeneratedComInterfaceAttribute para marcar una interfaz como una interfaz COM para el generador de origen. A continuación, el generador de código fuente generará un código que habilite la llamada desde el código de C# al código no administrado. También genera un código para habilitar la llamada desde el código no administrado en C#. Este generador de origen se integra con LibraryImportAttribute y puede usar tipos con GeneratedComInterfaceAttribute como parámetros y tipos devueltos en métodos con atributos LibraryImport.

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

[GeneratedComInterface]
[Guid("5401c312-ab23-4dd3-aa40-3cb4b3a4683e")]
partial interface IComInterface
{
    void DoWork();
}

internal partial class MyNativeLib
{
    [LibraryImport(nameof(MyNativeLib))]
    public static partial void GetComInterface(out IComInterface comInterface);
}

El generador de código fuente también admite el nuevo atributo GeneratedComClassAttribute para permitirle pasar tipos que implementan interfaces con el atributo GeneratedComInterfaceAttribute al código no administrado. El generador de código fuente generará el código necesario para exponer un objeto COM que implemente las interfaces y reenvíe las llamadas a la implementación administrada.

Los métodos en interfaces con el atributo GeneratedComInterfaceAttribute admiten todos los mismos tipos que LibraryImportAttribute y ahora LibraryImportAttribute admite tipos con atributos GeneratedComInterface y tipos con atributos GeneratedComClass.

Si el código de C# solo usa una interfaz con atributos GeneratedComInterface para encapsular un objeto COM desde un código no administrado o encapsular un objeto administrado de C# para exponerlo a un código no administrado, puede usar las opciones de la propiedad Options para personalizar qué código se generará. Estas opciones significan que no es necesario escribir serializadores que sepa que no se usarán para los escenarios.

El generador de origen usa el nuevo tipo StrategyBasedComWrappers para crear y administrar los contenedores de objetos COM y los contenedores de objetos administrados. Este nuevo tipo controla el suministro de la experiencia de usuario de .NET esperada para la interoperabilidad COM, a la vez que proporciona puntos de personalización para los usuarios avanzados. Si la aplicación tiene su propio mecanismo para definir tipos de COM o si necesita admitir escenarios que el COM generado por el origen no admite actualmente, considere la posibilidad de usar el nuevo tipo StrategyBasedComWrappers para agregar las características que faltan para su escenario y obtener la misma experiencia de usuario de .NET para los tipos COM.

Si usa Visual Studio, las nuevas correcciones de analizadores y códigos facilitan la conversión del código de interoperabilidad COM existente para usar la interoperabilidad generada por el origen. Junto a cada interfaz que tiene ComImportAttribute, una bombilla ofrece una opción para convertir a la interoperabilidad generada por el origen. La corrección cambia la interfaz para usar el atributo GeneratedComInterfaceAttribute. Y junto a cada clase que implementa una interfaz con GeneratedComInterfaceAttribute, una bombilla ofrece una opción para agregar el atributo GeneratedComClassAttribute al tipo. Una vez convertidos los tipos, puede mover los métodos DllImport para usar LibraryImportAttribute.

Limitaciones

El generador de origen COM no admite la afinidad de apartamento, con la palabra clave new para activar una coclase COM y las SIGUIENTES API:

Generador de origen de enlace de configuración

.NET 8 presenta un generador de origen para proporcionar una configuración de AOT y fácil de recortar en ASP.NET Core. El generador es una alternativa a la implementación basada en reflexión preexistente.

El generador de origen sondea las llamadas Configure(TOptions), Bind y Get, desde las cuales recuperar información de tipo. Cuando el generador está habilitado en un proyecto, el compilador elige implícitamente los métodos generados en lugar de las implementaciones de marco basadas en reflexión preexistentes.

No se necesitan cambios en el código fuente para usar el generador. Está habilitado de forma predeterminada en las aplicaciones web de AOT. Para otros tipos de proyecto, el generador de origen está desactivado de forma predeterminada, pero puede optar por establecer la propiedad EnableConfigurationBindingGenerator en true en el archivo del proyecto:

<PropertyGroup>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

En el código siguiente, se muestra un ejemplo de cómo invocar al enlazador.

public class ConfigBindingSG
{
    static void RunIt(params string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        IConfigurationSection section = builder.Configuration.GetSection("MyOptions");

        // !! Configure call - to be replaced with source-gen'd implementation
        builder.Services.Configure<MyOptions>(section);

        // !! Get call - to be replaced with source-gen'd implementation
        MyOptions? options0 = section.Get<MyOptions>();

        // !! Bind call - to be replaced with source-gen'd implementation
        MyOptions options1 = new();
        section.Bind(options1);

        WebApplication app = builder.Build();
        app.MapGet("/", () => "Hello World!");
        app.Run();
    }

    public class MyOptions
    {
        public int A { get; set; }
        public string S { get; set; }
        public byte[] Data { get; set; }
        public Dictionary<string, string> Values { get; set; }
        public List<MyClass> Values2 { get; set; }
    }

    public class MyClass
    {
        public int SomethingElse { get; set; }
    }
}

Bibliotecas de Core .NET

Esta sección contiene los subtemas siguientes:

Reflexión

Los punteros de función se introdujeron en .NET 5, pero la compatibilidad correspondiente con la reflexión no se agregó en ese momento. Cuando se usa typeof o la reflexión en un puntero de función, por ejemplo, typeof(delegate*<void>()) o FieldInfo.FieldType respectivamente, se devuelve un objeto IntPtr. A partir de .NET 8, se devuelve un objeto System.Type, alternativamente. Este tipo proporciona acceso a los metadatos del puntero de función, incluidas las convenciones de llamada, el tipo de valor devuelto y los parámetros.

Nota:

Una instancia de puntero de función, que es una dirección física de una función, continúa representándose como IntPtr. Solo ha cambiado el tipo de reflexión.

La nueva funcionalidad solo se implementa actualmente en el entorno de ejecución de CoreCLR y MetadataLoadContext.

Se han agregado nuevas API a System.Type, como IsFunctionPointer, y a System.Reflection.PropertyInfo, System.Reflection.FieldInfoy System.Reflection.ParameterInfo. En el código siguiente se muestra cómo usar algunas de las nuevas API para la reflexión.

using System;
using System.Reflection;

// Sample class that contains a function pointer field.
public unsafe class UClass
{
    public delegate* unmanaged[Cdecl, SuppressGCTransition]<in int, void> _fp;
}

internal class FunctionPointerReflection
{
    public static void RunIt()
    {
        FieldInfo? fieldInfo = typeof(UClass).GetField(nameof(UClass._fp));

        // Obtain the function pointer type from a field.
        Type? fpType = fieldInfo?.FieldType;

        // New methods to determine if a type is a function pointer.
        Console.WriteLine(
        $"IsFunctionPointer: {fpType?.IsFunctionPointer}");
        Console.WriteLine(
            $"IsUnmanagedFunctionPointer: {fpType?.IsUnmanagedFunctionPointer}");

        // New methods to obtain the return and parameter types.
        Console.WriteLine($"Return type: {fpType?.GetFunctionPointerReturnType()}");

        if (fpType is not null)
        {
            foreach (Type parameterType in fpType.GetFunctionPointerParameterTypes())
            {
                Console.WriteLine($"Parameter type: {parameterType}");
            }
        }

        // Access to custom modifiers and calling conventions requires a "modified type".
        Type? modifiedType = fieldInfo?.GetModifiedFieldType();

        // A modified type forwards most members to its underlying type.
        Type? normalType = modifiedType?.UnderlyingSystemType;

        if (modifiedType is not null)
        {
            // New method to obtain the calling conventions.
            foreach (Type callConv in modifiedType.GetFunctionPointerCallingConventions())
            {
                Console.WriteLine($"Calling convention: {callConv}");
            }
        }

        // New method to obtain the custom modifiers.
        Type[]? modifiers =
            modifiedType?.GetFunctionPointerParameterTypes()[0].GetRequiredCustomModifiers();

        if (modifiers is not null)
        {
            foreach (Type modreq in modifiers)
            {
                Console.WriteLine($"Required modifier for first parameter: {modreq}");
            }
        }
    }
}

En el ejemplo anterior se genera la salida siguiente:

IsFunctionPointer: True
IsUnmanagedFunctionPointer: True
Return type: System.Void
Parameter type: System.Int32&
Calling convention: System.Runtime.CompilerServices.CallConvSuppressGCTransition
Calling convention: System.Runtime.CompilerServices.CallConvCdecl
Required modifier for first parameter: System.Runtime.InteropServices.InAttribute

Serialización

Se han realizado varias mejoras en la funcionalidad de serialización y deserialización de System.Text.Json en .NET 8. Por ejemplo, puede personalizar el control de los miembros que no están en la carga JSON.

En las secciones siguientes se describen otras mejoras de serialización:

Para obtener más información sobre la serialización de JSON en general, consulte Serialización y deserialización de JSON en .NET.

Compatibilidad integrada con tipos adicionales

El serializador tiene compatibilidad integrada con los siguientes tipos adicionales.

  • Tipos numéricos Half, Int128 y UInt128.

    Console.WriteLine(JsonSerializer.Serialize(
        [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ]
    ));
    // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
    
  • Valores Memory<T> y ReadOnlyMemory<T>. Los valores byte se serializan en cadenas Base64 y otros tipos en matrices JSON.

    JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1, 2, 3 }); // "AQID"
    JsonSerializer.Serialize<Memory<int>>(new int[] { 1, 2, 3 }); // [1,2,3]
    

Generador de origen

En .NET 8 se incluyen mejoras del generador de código fuente System.Text.Json destinadas a equiparar la experiencia de AOT nativa con el serializador basado en reflexión. Por ejemplo:

  • El generador de origen ahora admite la serialización de tipos con propiedades required y init. Ambas propiedades ya se admitían en la serialización basada en reflexión.

  • Se ha mejorado el formato del código generado por el código fuente.

  • Paridad de características de JsonSourceGenerationOptionsAttribute con JsonSerializerOptions. Para obtener más información, vea Especificar opciones (generación de origen).

  • Diagnósticos adicionales (como SYSLIB1034 y SYSLIB1039).

  • No incluya tipos de propiedades ignoradas o inaccesibles.

  • Compatibilidad con declaraciones de anidamiento JsonSerializerContext dentro de tipos arbitrarios.

  • Agrega compatibilidad con los tipos generados por el compilador o indescriptibles en escenarios de generación de origen débilmente tipados. Dado que el generador de origen no puede especificar explícitamente los tipos generados por el compilador, System.Text.Json ahora realiza la resolución de antecesor más próximo en tiempo de ejecución. Esta resolución determina el supertipo más adecuado con el que serializar el valor.

  • Nuevo tipo de convertidor JsonStringEnumConverter<TEnum>. La clase JsonStringEnumConverter existente no se admite en AOT nativa. Puede anotar los tipos de enumeración de la siguiente manera:

    [JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
    public enum MyEnum { Value1, Value2, Value3 }
    
    [JsonSerializable(typeof(MyEnum))]
    public partial class MyContext : JsonSerializerContext { }
    

    Para obtener más información, consulte Serialización de campos de enumeración como cadenas.

  • La nueva propiedad JsonConverter.Type permite buscar el tipo de una instancia JsonConverter no genérica:

    Dictionary<Type, JsonConverter> CreateDictionary(IEnumerable<JsonConverter> converters)
        => converters.Where(converter => converter.Type != null)
                     .ToDictionary(converter => converter.Type!);
    

    La propiedad acepta valores NULL, ya que devuelve null para las instancias JsonConverterFactory y typeof(T) para las instancias JsonConverter<T>.

Generadores de origen de cadena

La clase JsonSerializerOptions incluye una nueva propiedad TypeInfoResolverChain que complementa la propiedad TypeInfoResolver existente. Estas propiedades se usan en la personalización del contrato para encadenar generadores de origen. La adición de la nueva propiedad significa que no es necesario especificar todos los componentes encadenados en un sitio de llamada; se pueden agregar después del hecho. TypeInfoResolverChain también permite hacer una introspección de la cadena o quitar componentes de ella. Para obtener más información, consulte Combinar generadores de origen.

Además, JsonSerializerOptions.AddContext<TContext>() ahora está obsoleto. Se ha reemplazado por las propiedades TypeInfoResolver y TypeInfoResolverChain. Para obtener más información, consulte SYSLIB0049.

Jerarquías de interfaz

.NET 8 agrega compatibilidad con la serialización de propiedades desde jerarquías de interfaz.

En el código siguiente se muestra un ejemplo de serialización de las propiedades desde la interfaz implementada inmediatamente y desde su interfaz base.

public static void InterfaceHierarchies()
{
    IDerived value = new DerivedImplement { Base = 0, Derived = 1 };
    string json = JsonSerializer.Serialize(value);
    Console.WriteLine(json); // {"Derived":1,"Base":0}
}

public interface IBase
{
    public int Base { get; set; }
}

public interface IDerived : IBase
{
    public int Derived { get; set; }
}

public class DerivedImplement : IDerived
{
    public int Base { get; set; }
    public int Derived { get; set; }
}

Directivas de nomenclatura

JsonNamingPolicy incluye nuevas directivas de nomenclatura para las conversiones de nombres de propiedad snake_case (con guion bajo) y kebab-case (con guion). Use estas directivas de forma similar a la directiva JsonNamingPolicy.CamelCase existente:

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
JsonSerializer.Serialize(new { PropertyName = "value" }, options);
// { "property_name" : "value" }

Para obtener más información, consulte Uso de una directiva de nomenclatura integrada.

Propiedades de solo lectura

Ahora puede deserializar en campos o propiedades de solo lectura (es decir, aquellos que no tienen un descriptor de acceso set).

Para participar en esta característica globalmente, establezca una nueva opción, PreferredObjectCreationHandling, en JsonObjectCreationHandling.Populate. Si la compatibilidad es un problema, también puede habilitar la funcionalidad de forma más granular colocando el atributo [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] en tipos específicos cuyas propiedades se van a rellenar o en propiedades individuales.

Por ejemplo, considere el código siguiente que deserializa en un tipo CustomerInfo que tiene dos propiedades de solo lectura.

public static void ReadOnlyProperties()
{
    CustomerInfo customer = JsonSerializer.Deserialize<CustomerInfo>("""
        { "Names":["John Doe"], "Company":{"Name":"Contoso"} }
        """)!;

    Console.WriteLine(JsonSerializer.Serialize(customer));
}

class CompanyInfo
{
    public required string Name { get; set; }
    public string? PhoneNumber { get; set; }
}

[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class CustomerInfo
{
    // Both of these properties are read-only.
    public List<string> Names { get; } = new();
    public CompanyInfo Company { get; } = new()
    {
        Name = "N/A",
        PhoneNumber = "N/A"
    };
}

Antes de .NET 8, los valores de entrada se ignoraban y las propiedades Names y Company conservaban sus valores predeterminados.

{"Names":[],"Company":{"Name":"N/A","PhoneNumber":"N/A"}}

Ahora, los valores de entrada se usan para rellenar las propiedades de solo lectura durante la deserialización.

{"Names":["John Doe"],"Company":{"Name":"Contoso","PhoneNumber":"N/A"}}

Para obtener más información sobre el rellenar comportamiento de deserialización, vea Rellenar propiedades inicializadas.

Deshabilitación del valor predeterminado basado en reflexión

Ahora puede deshabilitar el uso del serializador basado en reflexión de forma predeterminada. Esta deshabilitación es útil para evitar crear una raíz accidental de componentes de reflexión que ni siquiera están en uso, especialmente en aplicaciones AOT recortadas y nativas. Para deshabilitar la serialización basada en reflexión predeterminada de manera que sea necesario pasar un argumento JsonSerializerOptions a los métodos de serialización y deserialización de JsonSerializer, establezca la propiedad MSBuild JsonSerializerIsReflectionEnabledByDefault en false en el archivo del proyecto.

Use la nueva API IsReflectionEnabledByDefault para comprobar el valor del modificador de características. Si es un autor de biblioteca que se basa en System.Text.Json, puede confiar en la propiedad para configurar los valores predeterminados evitando crear una raíz accidental de los componentes de reflexión.

Para obtener más información, vea Deshabilitar los valores predeterminados de reflexión.

Nuevos métodos de API de JsonNode

Los tipos JsonNode y System.Text.Json.Nodes.JsonArray incluyen los siguientes métodos nuevos.

public partial class JsonNode
{
    // Creates a deep clone of the current node and all its descendants.
    public JsonNode DeepClone();

    // Returns true if the two nodes are equivalent JSON representations.
    public static bool DeepEquals(JsonNode? node1, JsonNode? node2);

    // Determines the JsonValueKind of the current node.
    public JsonValueKind GetValueKind(JsonSerializerOptions options = null);

    // If node is the value of a property in the parent
    // object, returns its name.
    // Throws InvalidOperationException otherwise.
    public string GetPropertyName();

    // If node is the element of a parent JsonArray,
    // returns its index.
    // Throws InvalidOperationException otherwise.
    public int GetElementIndex();

    // Replaces this instance with a new value,
    // updating the parent object/array accordingly.
    public void ReplaceWith<T>(T value);

    // Asynchronously parses a stream as UTF-8 encoded data
    // representing a single JSON value into a JsonNode.
    public static Task<JsonNode?> ParseAsync(
        Stream utf8Json,
        JsonNodeOptions? nodeOptions = null,
        JsonDocumentOptions documentOptions = default,
        CancellationToken cancellationToken = default);
}

public partial class JsonArray
{
    // Returns an IEnumerable<T> view of the current array.
    public IEnumerable<T> GetValues<T>();
}

Miembros no públicos

Puede optar por miembros no públicos en el contrato de serialización de un tipo determinado mediante anotaciones de atributo JsonIncludeAttribute y JsonConstructorAttribute.

public static void NonPublicMembers()
{
    string json = JsonSerializer.Serialize(new MyPoco(42));
    Console.WriteLine(json);
    // {"X":42}

    JsonSerializer.Deserialize<MyPoco>(json);
}

public class MyPoco
{
    [JsonConstructor]
    internal MyPoco(int x) => X = x;

    [JsonInclude]
    internal int X { get; }
}

Para obtener más información, vea Usar tipos inmutables y descriptores de acceso y miembros no públicos.

API de deserialización de streaming

.NET 8 incluye nuevos métodos de extensión de deserialización de streaming IAsyncEnumerable<T>, por ejemplo GetFromJsonAsAsyncEnumerable. Existen métodos similares que devuelven Task<TResult>, por ejemplo, HttpClientJsonExtensions.GetFromJsonAsync. Los nuevos métodos de extensión invocan las API de streaming y devuelven IAsyncEnumerable<T>.

En el código siguiente se muestra cómo puede usar los nuevos métodos de extensión.

public async static void StreamingDeserialization()
{
    const string RequestUri = "https://api.contoso.com/books";
    using var client = new HttpClient();
    IAsyncEnumerable<Book?> books = client.GetFromJsonAsAsyncEnumerable<Book>(RequestUri);

    await foreach (Book? book in books)
    {
        Console.WriteLine($"Read book '{book?.title}'");
    }
}

public record Book(int id, string title, string author, int publishedYear);

Método de extensión WithAddedModifier

El nuevo método de extensión WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>) permite introducir fácilmente modificaciones en los contratos de serialización de instancias arbitrarias IJsonTypeInfoResolver.

var options = new JsonSerializerOptions
{
    TypeInfoResolver = MyContext.Default
        .WithAddedModifier(static typeInfo =>
        {
            foreach (JsonPropertyInfo prop in typeInfo.Properties)
            {
                prop.Name = prop.Name.ToUpperInvariant();
            }
        })
};

Nuevas sobrecargas JsonContent.Create

Ahora puede crear instancias JsonContent mediante contratos seguros frente a recortes o generados por el origen. Los nuevos métodos son:

var book = new Book(id: 42, "Title", "Author", publishedYear: 2023);
HttpContent content = JsonContent.Create(book, MyContext.Default.Book);

public record Book(int id, string title, string author, int publishedYear);

[JsonSerializable(typeof(Book))]
public partial class MyContext : JsonSerializerContext
{
}

Inmovilización de una instancia de JsonSerializerOptions

Los métodos nuevos siguientes permiten controlar cuándo se inmoviliza una instancia JsonSerializerOptions:

  • JsonSerializerOptions.MakeReadOnly()

    Esta sobrecarga está diseñada para ser segura frente a recortes y, por tanto, producirá una excepción en los casos en los que la instancia de opciones no se haya configurado con un solucionador.

  • JsonSerializerOptions.MakeReadOnly(Boolean)

    Si pasa true a esta sobrecarga, rellena la instancia de opciones con la resolución de reflexión predeterminada si falta una. Este método está marcado como RequiresUnreferenceCode/RequiresDynamicCode y, por tanto, no es adecuado para las aplicaciones AOT nativas.

La nueva propiedad IsReadOnly le permite comprobar si la instancia de opciones está inmovilizada.

Abstracción de tiempo

La nueva clase TimeProvider e interfaz ITimer agregan funcionalidad de abstracción de tiempo, que permite simular el tiempo en escenarios de prueba. Además, puede usar la abstracción de tiempo para simular operaciones Task que dependen de la progresión de tiempo mediante Task.Delay y Task.WaitAsync. La abstracción de tiempo admite las siguientes operaciones de tiempo esenciales:

  • Recuperación de la hora local y UTC
  • Obtención de una marca de tiempo para medir el rendimiento
  • Creación de un temporizador

En el fragmento de código siguiente se muestran algunos ejemplos de uso.

// Get system time.
DateTimeOffset utcNow = TimeProvider.System.GetUtcNow();
DateTimeOffset localNow = TimeProvider.System.GetLocalNow();

TimerCallback callback = s => ((State)s!).Signal();

// Create a timer using the time provider.
ITimer timer = _timeProvider.CreateTimer(
    callback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);

// Measure a period using the system time provider.
long providerTimestamp1 = TimeProvider.System.GetTimestamp();
long providerTimestamp2 = TimeProvider.System.GetTimestamp();

TimeSpan period = _timeProvider.GetElapsedTime(providerTimestamp1, providerTimestamp2);
// Create a time provider that works with a
// time zone that's different than the local time zone.
private class ZonedTimeProvider(TimeZoneInfo zoneInfo) : TimeProvider()
{
    private readonly TimeZoneInfo _zoneInfo = zoneInfo ?? TimeZoneInfo.Local;

    public override TimeZoneInfo LocalTimeZone => _zoneInfo;

    public static TimeProvider FromLocalTimeZone(TimeZoneInfo zoneInfo) =>
        new ZonedTimeProvider(zoneInfo);
}

Mejoras de UTF8

Si quiere habilitar la escritura de una representación similar a una cadena del tipo en un intervalo de destino, implemente la nueva interfaz IUtf8SpanFormattable en el tipo. Esta nueva interfaz está estrechamente relacionada con ISpanFormattable, pero tiene como destino UTF8 y Span<byte> en lugar de UTF16 y Span<char>.

IUtf8SpanFormattable se ha implementado en todos los tipos primitivos (y otros más), con la misma lógica compartida exactamente si el destino es string, Span<char> o Span<byte>. Tiene compatibilidad completa con todos los formatos (incluido el nuevo especificador binario "B") y todas las referencias culturales. Esto significa que ahora puede dar formato directamente a UTF8 desde Byte, Complex, Char, DateOnly, DateTime, DateTimeOffset, Decimal, Double, Guid, Half, IPAddress, IPNetwork, Int16, Int32, Int64, Int128, IntPtr, NFloat, SByte, Single, Rune, TimeOnly, TimeSpan, UInt16, UInt32, UInt64, UInt128, UIntPtr y Version.

Los nuevos métodos Utf8.TryWrite proporcionan un homólogo basado en UTF8 para los métodos MemoryExtensions.TryWrite existentes, que se basan en UTF16. Puede usar la sintaxis de cadena interpolada para dar formato a una expresión compleja directamente en un intervalo de bytes UTF8, por ejemplo:

static bool FormatHexVersion(
    short major,
    short minor,
    short build,
    short revision,
    Span<byte> utf8Bytes,
    out int bytesWritten) =>
    Utf8.TryWrite(
        utf8Bytes,
        CultureInfo.InvariantCulture,
        $"{major:X4}.{minor:X4}.{build:X4}.{revision:X4}",
        out bytesWritten);

La implementación reconoce IUtf8SpanFormattable en los valores de formato y usa sus implementaciones para escribir sus representaciones UTF8 directamente en el intervalo de destino.

La implementación también utiliza el nuevo método Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32), que junto con su homólogo Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32), admite la codificación y la descodificación en un intervalo de destino. Si el intervalo no es lo suficientemente largo como para contener el estado resultante, los métodos devuelven false en lugar de producir una excepción.

Métodos para trabajar con aleatoriedad

Los tipos System.Random y System.Security.Cryptography.RandomNumberGenerator presentan dos nuevos métodos para trabajar con aleatoriedad.

GetItems<T>()

Los nuevos métodos System.Random.GetItems y System.Security.Cryptography.RandomNumberGenerator.GetItems permiten elegir aleatoriamente un número especificado de elementos de un conjunto de entrada. En el ejemplo siguiente se muestra cómo usar System.Random.GetItems<T>() (en la instancia proporcionada por la propiedad Random.Shared) para insertar aleatoriamente 31 elementos en una matriz. Este ejemplo podría usarse en un juego "Simon", donde los jugadores deben recordar una secuencia de botones de colores.

private static ReadOnlySpan<Button> s_allButtons = new[]
{
    Button.Red,
    Button.Green,
    Button.Blue,
    Button.Yellow,
};

// ...

Button[] thisRound = Random.Shared.GetItems(s_allButtons, 31);
// Rest of game goes here ...

Shuffle<T>()

Los nuevos métodos Random.Shuffle y RandomNumberGenerator.Shuffle<T>(Span<T>) permiten aleatorizar el orden de un intervalo. Estos métodos son útiles para reducir el sesgo de entrenamiento en el aprendizaje automático (para que lo primero no sea siempre el entrenamiento y lo último las pruebas).

YourType[] trainingData = LoadTrainingData();
Random.Shared.Shuffle(trainingData);

IDataView sourceData = mlContext.Data.LoadFromEnumerable(trainingData);

DataOperationsCatalog.TrainTestData split = mlContext.Data.TrainTestSplit(sourceData);
model = chain.Fit(split.TrainSet);

IDataView predictions = model.Transform(split.TestSet);
// ...

Tipos centrados en el rendimiento

.NET 8 presenta varios tipos nuevos destinados a mejorar el rendimiento de la aplicación.

  • El nuevo espacio de nombres System.Collections.Frozen incluye los tipos de colección FrozenDictionary<TKey,TValue> y FrozenSet<T>. Estos tipos no permiten cambios en las claves y los valores una vez creada una colección. Ese requisito permite operaciones de lectura más rápidas (por ejemplo, TryGetValue()). Además, estos tipos son especialmente útiles para las colecciones que se rellenan en el primer uso y que se conservan durante un servicio de larga duración, por ejemplo:

    private static readonly FrozenDictionary<string, bool> s_configurationData =
        LoadConfigurationData().ToFrozenDictionary(optimizeForReads: true);
    
    // ...
    if (s_configurationData.TryGetValue(key, out bool setting) && setting)
    {
        Process();
    }
    
  • Métodos como MemoryExtensions.IndexOfAny buscan la primera aparición de cualquier valor en la colección pasada. El nuevo tipo de System.Buffers.SearchValues<T> está diseñado para pasarse a dichos métodos. En consecuencia, .NET 8 agrega nuevas sobrecargas de métodos como MemoryExtensions.IndexOfAny que aceptan una instancia del nuevo tipo. Cuando se crea una instancia de SearchValues<T>, todos los datos necesarios para optimizar las búsquedas posteriores se obtienen en ese momento, lo que significa que el trabajo se realiza por adelantado.

  • El nuevo tipo System.Text.CompositeFormat es útil para optimizar las cadenas de formato que no se conocen en tiempo de compilación (por ejemplo, si la cadena de formato se carga desde un archivo de recursos). Se dedica un poco más de tiempo al trabajo previo, como analizar la cadena, pero esto evita que el trabajo se realice en cada uso.

    private static readonly CompositeFormat s_rangeMessage =
        CompositeFormat.Parse(LoadRangeMessageResource());
    
    // ...
    static string GetMessage(int min, int max) =>
        string.Format(CultureInfo.InvariantCulture, s_rangeMessage, min, max);
    
  • Los nuevos tipos System.IO.Hashing.XxHash3 y System.IO.Hashing.XxHash128 proporcionan implementaciones de los algoritmos hash XXH3 y XXH128 rápidos.

System.Numerics and System.Runtime.Intrinsics

En esta sección se describen las mejoras en los espacios de nombres System.Numerics y System.Runtime.Intrinsics.

  • Vector256<T>, Matrix3x2 y Matrix4x4 han mejorado la aceleración de hardware en .NET 8. Por ejemplo, se ha vuelto a implementar Vector256<T> para tener operaciones 2x Vector128<T> internas, siempre que sea posible. Esto permite la aceleración parcial de algunas funciones cuando Vector128.IsHardwareAccelerated == true pero Vector256.IsHardwareAccelerated == false, como en Arm64.
  • Los elementos intrínsecos de hardware ahora se anotan con el atributo ConstExpected. Esto garantiza que los usuarios sepan cuándo el hardware subyacente espera una constante y, por tanto, cuándo un valor que no es una constante puede dañar inesperadamente el rendimiento.
  • La API Lerp(TSelf, TSelf, TSelf)Lerp se ha agregado a IFloatingPointIeee754<TSelf> y, por tanto, a float (Single), double (Double) y Half. Esta API permite que se realice una interpolación lineal entre dos valores de forma eficaz y correcta.

Vector512 y AVX-512

.NET Core 3.0 amplió la compatibilidad con SIMD para incluir las API intrínsecas de hardware específicas de la plataforma para x86/x64. .NET 5 agregó compatibilidad con Arm64 y .NET 7 agregó los intrínsecos de hardware multiplataforma. .NET 8 amplía la compatibilidad con SIMD al introducir Vector512<T> y admitir las instrucciones Extensiones de vector avanzadas 512 (AVX-512) de Intel.

En concreto, .NET 8 incluye compatibilidad con las siguientes características clave de AVX-512:

  • Operaciones vectoriales de 512 bits
  • 16 registros SIMD adicionales
  • Instrucciones adicionales disponibles para vectores de 128 bits, 256 y 512 bits

Si tiene hardware que admite la funcionalidad, Vector512.IsHardwareAccelerated ahora notifica true.

.NET 8 también agrega varias clases específicas de la plataforma en el espacio de nombres System.Runtime.Intrinsics.X86:

Estas clases siguen la misma forma general que otras arquitecturas de conjuntos de instrucciones (ISA) en que exponen una propiedad IsSupported y una clase Avx512F.X64 anidada para las instrucciones disponibles solo para procesos de 64 bits. Además, cada clase tiene una clase Avx512F.VL anidada que expone las extensiones Avx512VL (longitud del vector) para el conjunto de instrucciones correspondiente.

Incluso si no usa explícitamente instrucciones específicas de Vector512 o específicas de Avx512F en el código, es probable que se beneficie de la nueva compatibilidad con AVX-512. JIT puede aprovechar los registros e instrucciones adicionales implícitamente al usar Vector128<T> o Vector256<T>. La biblioteca de clases base usa estos intrínsecos de hardware internamente en la mayoría de las operaciones expuestas por Span<T> y ReadOnlySpan<T> en muchas de las API matemáticas expuestas para los tipos primitivos.

Validación de datos

El espacio de nombres System.ComponentModel.DataAnnotations incluye nuevos atributos de validación de datos destinados a escenarios de validación en servicios nativos en la nube. Aunque los validadores preexistentes DataAnnotations están orientados a la validación típica de entrada de datos de la interfaz de usuario, como los campos de un formulario, los nuevos atributos están diseñados para validar datos que no son de entrada de usuario, como las opciones de configuración. Además de los nuevos atributos, se agregaron nuevas propiedades a los tipos RangeAttribute y RequiredAttribute.

Nueva API Descripción
RangeAttribute.MinimumIsExclusive
RangeAttribute.MaximumIsExclusive
Especifica si los límites se incluyen en el intervalo permitido.
System.ComponentModel.DataAnnotations.LengthAttribute Especifica los límites inferiores y superiores de las cadenas o colecciones. Por ejemplo, [Length(10, 20)] requiere al menos 10 elementos y como máximo 20 elementos en una colección.
System.ComponentModel.DataAnnotations.Base64StringAttribute Valida que una cadena es una representación válida de Base64.
System.ComponentModel.DataAnnotations.AllowedValuesAttribute
System.ComponentModel.DataAnnotations.DeniedValuesAttribute
Especifique listas de permitidos y listas de denegación, respectivamente. Por ejemplo, [AllowedValues("apple", "banana", "mango")].

Métricas

Las nuevas API permiten asociar etiquetas de par clave-valor a objetos Meter y Instrument al crearlos. Los agregadores de unidades métricas publicadas pueden usar las etiquetas para diferenciar los valores agregados.

var options = new MeterOptions("name")
{
    Version = "version",
    // Attach these tags to the created meter.
    Tags = new TagList()
    {
        { "MeterKey1", "MeterValue1" },
        { "MeterKey2", "MeterValue2" }
    }
};

Meter meter = meterFactory!.Create(options);

Counter<int> counterInstrument = meter.CreateCounter<int>(
    "counter", null, null, new TagList() { { "counterKey1", "counterValue1" } }
);
counterInstrument.Add(1);

Entre las nuevas API se incluyen:

Criptografía

.NET 8 agrega compatibilidad con los primitivos hash SHA-3. (SHA-3 es compatible actualmente con Linux con OpenSSL 1.1.1 o posterior y la compilación 25324 de Windows 11 o posterior). Las API en las que SHA-2 está disponible ahora ofrecen un complemento SHA-3. Esto incluye SHA3_256, SHA3_384 y SHA3_512 para el hash; HMACSHA3_256, HMACSHA3_384 y HMACSHA3_512 para HMAC; HashAlgorithmName.SHA3_256, HashAlgorithmName.SHA3_384 y HashAlgorithmName.SHA3_512 para el hash donde se puede configurar el algoritmo; y RSAEncryptionPadding.OaepSHA3_256, RSAEncryptionPadding.OaepSHA3_384 y RSAEncryptionPadding.OaepSHA3_512 para el cifrado OAEP RSA.

En el ejemplo siguiente se muestra cómo usar las API, incluida la propiedad SHA3_256.IsSupported para determinar si la plataforma admite SHA-3.

// Hashing example
if (SHA3_256.IsSupported)
{
    byte[] hash = SHA3_256.HashData(dataToHash);
}
else
{
    // ...
}

// Signing example
if (SHA3_256.IsSupported)
{
     using ECDsa ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
     byte[] signature = ec.SignData(dataToBeSigned, HashAlgorithmName.SHA3_256);
}
else
{
    // ...
}

La compatibilidad con SHA-3 está destinada actualmente a admitir primitivos criptográficos. No se espera que las construcciones y protocolos de nivel superior admitan SHA-3 completamente al principio. Estos protocolos incluyen los certificados X.509, SignedXml y COSE.

Redes

Compatibilidad con el proxy HTTPS

Hasta ahora, todos los tipos de proxy que HttpClient admitía permitían un "man in the middle" para ver a qué sitio se conecta el cliente, incluso para los URI HTTPS. HttpClient ahora admite el proxy HTTPS, que crea un canal cifrado entre el cliente y el proxy para que todas las solicitudes se puedan controlar con plena privacidad.

Para habilitar el proxy HTTPS, establezca la variable de entorno all_proxy o use la clase WebProxy para controlar el proxy mediante programación.

Unix: export all_proxy=https://x.x.x.x:3218 Windows: set all_proxy=https://x.x.x.x:3218

También puede usar la clase WebProxy para controlar el proxy mediante programación.

Métodos ZipFile basados en secuencias

.NET 8 incluye nuevas sobrecargas de ZipFile.CreateFromDirectory que le permiten recopilar todos los archivos incluidos en un directorio y comprimirlos y, a continuación, almacenar el archivo ZIP resultante en la secuencia proporcionada. De forma similar, las nuevas sobrecargas ZipFile.ExtractToDirectory permiten proporcionar una secuencia que contiene un archivo comprimido y extraer su contenido en el sistema de archivos. Estas son las nuevas sobrecargas:

namespace System.IO.Compression;

public static partial class ZipFile
{
    public static void CreateFromDirectory(
        string sourceDirectoryName, Stream destination);

    public static void CreateFromDirectory(
        string sourceDirectoryName,
        Stream destination,
        CompressionLevel compressionLevel,
        bool includeBaseDirectory);

    public static void CreateFromDirectory(
        string sourceDirectoryName,
        Stream destination,
        CompressionLevel compressionLevel,
        bool includeBaseDirectory,
    Encoding? entryNameEncoding);

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, bool overwriteFiles) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) { }

    public static void ExtractToDirectory(
        Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
}

Estas nuevas API pueden ser útiles cuando se restringe el espacio en disco, ya que evitan tener que usar el disco como paso intermedio.

Bibliotecas de extensiones

Esta sección contiene los subtemas siguientes:

Servicios de inserción de dependencias con claves

Los servicios de inserción de dependencias (DI) con claves proporcionan un medio para registrar y recuperar servicios de inserción de dependencias mediante claves. Mediante el uso de claves, es posible definir el ámbito de cómo registrar y consumir servicios. Estas son algunas de las nuevas API:

En el ejemplo siguiente se muestra cómo usar los servicios de inserción de dependencias con clave.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<BigCacheConsumer>();
builder.Services.AddSingleton<SmallCacheConsumer>();
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
WebApplication app = builder.Build();
app.MapGet("/big", (BigCacheConsumer data) => data.GetData());
app.MapGet("/small", (SmallCacheConsumer data) => data.GetData());
app.MapGet("/big-cache", ([FromKeyedServices("big")] ICache cache) => cache.Get("data"));
app.MapGet("/small-cache", (HttpContext httpContext) => httpContext.RequestServices.GetRequiredKeyedService<ICache>("small").Get("data"));
app.Run();

class BigCacheConsumer([FromKeyedServices("big")] ICache cache)
{
    public object? GetData() => cache.Get("data");
}

class SmallCacheConsumer(IServiceProvider serviceProvider)
{
    public object? GetData() => serviceProvider.GetRequiredKeyedService<ICache>("small").Get("data");
}

public interface ICache
{
    object Get(string key);
}

public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

Para obtener más información, consulte dotnet/runtime#64427.

Servicios de ciclo de vida hospedados

Los servicios hospedados ahora tienen más opciones para la ejecución durante el ciclo de vida de la aplicación. IHostedService proporcionó StartAsync y StopAsync, y ahora IHostedLifecycleService proporciona estos métodos adicionales:

Estos métodos se ejecutan antes y después de los puntos existentes, respectivamente.

En el ejemplo siguiente, se muestra cómo usar laa nuevas API.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

internal class HostedLifecycleServices
{
    public async static void RunIt()
    {
        IHostBuilder hostBuilder = new HostBuilder();
        hostBuilder.ConfigureServices(services =>
        {
            services.AddHostedService<MyService>();
        });

        using (IHost host = hostBuilder.Build())
        {
            await host.StartAsync();
        }
    }

    public class MyService : IHostedLifecycleService
    {
        public Task StartingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StartedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StopAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
        public Task StoppingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
    }
}

Para obtener más información, consulte dotnet/runtime#86511.

Opciones de validación

Generador de origen

Para reducir la sobrecarga de inicio y mejorar el conjunto de características de validación, hemos introducido un generador de código fuente que implementa la lógica de validación. En el código siguiente se muestran modelos de ejemplo y clases de validadores.

public class FirstModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P1 { get; set; } = string.Empty;

    [Microsoft.Extensions.Options.ValidateObjectMembers(
        typeof(SecondValidatorNoNamespace))]
    public SecondModelNoNamespace? P2 { get; set; }
}

public class SecondModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P4 { get; set; } = string.Empty;
}

[OptionsValidator]
public partial class FirstValidatorNoNamespace
    : IValidateOptions<FirstModelNoNamespace>
{
}

[OptionsValidator]
public partial class SecondValidatorNoNamespace
    : IValidateOptions<SecondModelNoNamespace>
{
}

Si su aplicación usa la inserción de dependencias, puede insertar la validación como se muestra en el código de ejemplo siguiente.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<FirstModelNoNamespace>(
    builder.Configuration.GetSection("some string"));

builder.Services.AddSingleton<
    IValidateOptions<FirstModelNoNamespace>, FirstValidatorNoNamespace>();
builder.Services.AddSingleton<
    IValidateOptions<SecondModelNoNamespace>, SecondValidatorNoNamespace>();

Tipo ValidateOptionsResultBuilder

.NET 8 presenta el tipo ValidateOptionsResultBuilder para facilitar la creación de un objeto ValidateOptionsResult. Es importante destacar que este generador permite la acumulación de varios errores. Anteriormente, crear el objeto ValidateOptionsResult que se requiere para implementar IValidateOptions<TOptions>.Validate(String, TOptions) era difícil y, a veces, generaba errores de validación en capas. Si se producían muchos errores, el proceso de validación solía detenerse en el primer error.

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

ValidateOptionsResultBuilder builder = new();
builder.AddError("Error: invalid operation code");
builder.AddResult(ValidateOptionsResult.Fail("Invalid request parameters"));
builder.AddError("Malformed link", "Url");

// Build ValidateOptionsResult object has accumulating multiple errors.
ValidateOptionsResult result = builder.Build();

// Reset the builder to allow using it in new validation operation.
builder.Clear();

Constructores LoggerMessageAttribute

LoggerMessageAttribute ahora ofrece sobrecargas de constructor adicionales. Anteriormente, tenía que elegir el constructor sin parámetros o el constructor que requería todos los parámetros (identificador de evento, nivel de registro y mensaje). Las nuevas sobrecargas ofrecen mayor flexibilidad para especificar los parámetros necesarios con código reducido. Si no proporciona un identificador de evento, el sistema genera uno automáticamente.

public LoggerMessageAttribute(LogLevel level, string message);
public LoggerMessageAttribute(LogLevel level);
public LoggerMessageAttribute(string message);

Métricas de extensiones

Interfaz IMeterFactory

Puede registrar la nueva interfaz IMeterFactory en contenedores de inserción de dependencias y usarla para crear objetos Meter de forma aislada.

Registre la interfaz IMeterFactory en el contenedor de inserción de dependencias mediante la implementación predeterminada del generador de medidores:

// 'services' is the DI IServiceCollection.
services.AddMetrics();

A continuación, los consumidores pueden obtener el generador de medidores y usarlo para crear un objeto Meter.

IMeterFactory meterFactory = serviceProvider.GetRequiredService<IMeterFactory>();

MeterOptions options = new MeterOptions("MeterName")
{
    Version = "version",
};

Meter meter = meterFactory.Create(options);

Clase MetricCollector<T>

La nueva clase MetricCollector<T> permite registrar mediciones de métricas junto con marcas de tiempo. Además, la clase ofrece la flexibilidad de usar un proveedor de tiempo de su elección para generar una marca de tiempo precisa.

const string CounterName = "MyCounter";
DateTimeOffset now = DateTimeOffset.Now;

var timeProvider = new FakeTimeProvider(now);
using var meter = new Meter(Guid.NewGuid().ToString());
Counter<long> counter = meter.CreateCounter<long>(CounterName);
using var collector = new MetricCollector<long>(counter, timeProvider);

Assert.IsNull(collector.LastMeasurement);

counter.Add(3);

// Verify the update was recorded.
Assert.AreEqual(counter, collector.Instrument);
Assert.IsNotNull(collector.LastMeasurement);

Assert.AreSame(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement);
Assert.AreEqual(3, collector.LastMeasurement.Value);
Assert.AreEqual(now, collector.LastMeasurement.Timestamp);

System.Numerics.Tensors.TensorPrimitives

El paquete NuGet actualizado System.Numerics.Tensors incluye API en el nuevo espacio de nombres TensorPrimitives que agregan compatibilidad con operaciones de tensor. Los primitivos de tensor optimizan cargas de trabajo que consumen muchos datos, como las de inteligencia artificial y aprendizaje automático.

Las cargas de trabajo de inteligencia artificial, como la búsqueda semántica y la generación aumentada de recuperación (RAG) amplían las funcionalidades de lenguaje natural de modelos de lenguaje grandes, como ChatGPT, aumentando las solicitudes con datos relevantes. Para estas cargas de trabajo, las operaciones sobre vectores (como la similitud de coseno para encontrar los datos más relevantes para responder a una pregunta) son cruciales. El paquete System.Numerics.Tensors.TensorPrimitives proporciona API para las operaciones vectoriales, lo que significa que no es necesario tomar una dependencia externa ni escribir su propia implementación.

Este paquete reemplaza al paquete System.Numerics.Tensors.

Para más información, consulte la entrada de blog de Anuncio de .NET 8 RC 2.

Consulte también