Novidades no runtime do .NET 8

Este artigo descreve novos recursos no runtime do .NET para .NET 8.

Aprimoramentos de desempenho

O .NET 8 inclui melhorias na geração de código e na compilação JIT (just-in-time):

  • Aprimoramentos no desempenho do Arm64
  • Aprimoramentos no SIMD
  • Suporte para extensões ISA AVX-512 (confira Vector512 e AVX-512)
  • Aprimoramentos nativos de nuvem
  • Aprimoramentos na taxa de transferência JIT
  • Otimizações gerais e do loop
  • Acesso otimizado para campos marcados com ThreadStaticAttribute
  • Alocação de registro consecutiva. O Arm64 tem duas instruções para pesquisa de vetor de tabela, que exigem que todas as entidades em seus operandos de tupla estejam presentes em registros consecutivos.
  • JIT/NativeAOT agora pode cancelar o registro e vetorizar automaticamente algumas operações de memória com SIMD, como comparação, cópia e zero, se puder determinar seus tamanhos em tempo de compilação.

Além disso, a PGO (otimização guiada por perfil dinâmico) foi aprimorada e agora está habilitada por padrão. Você não precisa mais usar uma opção de configuração de runtime para habilitá-la. O PGO dinâmico funciona de forma estreita com a compilação em camadas para otimizar ainda mais o código com base na instrumentação adicional que é implementada durante a camada 0.

Em média, o PGO dinâmico aumenta o desempenho em cerca de 15%. Em um conjunto de parâmetros de comparação de ~4600 testes, 23% tiveram melhorias de desempenho de 20% ou mais.

Promoção de struct codegen

O .NET 8 inclui um novo passe de otimização de promoção física para codegen que generaliza a capacidade do JIT de promover variáveis de struct. Essa otimização (também chamada de substituição escalar de agregações) substitui os campos de variáveis struct por variáveis primitivas que o JIT é capaz de raciocinar e otimizar com mais precisão.

O JIT já tinha suporte para essa otimização, mas com várias limitações grandes, incluindo:

  • Ele só tinha suporte para structs com quatro ou menos campos.
  • Só havia suporte se cada campo fosse um tipo primitivo ou um struct simples encapsulando um tipo primitivo.

A promoção física remove essas limitações, o que corrige vários problemas de JIT de longa data.

Coleta de lixo

O .NET 8 adiciona uma funcionalidade para ajustar o limite de memória em tempo real. Isso é útil em cenários de serviço de nuvem, onde a demanda vem e vai. Para serem econômicos, os serviços devem escalar e reduzir verticalmente o consumo de recursos à medida que a demanda flutua. Quando um serviço detecta uma diminuição na demanda, ele pode reduzir o consumo de recursos reduzindo seu limite de memória. Anteriormente, isso falhava porque o coletor de lixo (GC) não sabia da alteração e poderia alocar mais memória do que o novo limite. Com essa alteração, você pode chamar a API RefreshMemoryLimit() para atualizar o GC com o novo limite de memória.

Existem algumas limitações a serem consideradas, como:

  • Em plataformas de 32 bits (por exemplo, Windows x86 e Linux ARM), o .NET não poderá estabelecer um novo limite rígido de heap se ainda não houver um.
  • A API pode retornar um código de status diferente de zero indicando que a atualização falhou. Isso pode acontecer se a redução horizontal for muito agressiva e não deixar espaço para a GC manobrar. Nesse caso, chame GC.Collect(2, GCCollectionMode.Aggressive) para reduzir o uso de memória atual e tente novamente.
  • Se você escalar verticalmente o limite de memória além do tamanho que o GC acredita que o processo pode lidar durante a inicialização, a chamada RefreshMemoryLimit terá êxito, mas não poderá usar mais memória do que o que ele percebe como o limite.

O snippet de código a seguir mostra como chamar a API.

GC.RefreshMemoryLimit();

Você também pode atualizar algumas das configurações de GC relacionadas ao limite de memória. O snippet de código a seguir define o limite rígido do heap como 100 mebibytes (MiB):

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

A API pode gerar um InvalidOperationException se o limite rígido for inválido, por exemplo, no caso de percentuais de limite rígido de heap negativo e se o limite rígido for muito baixo. Isso pode acontecer se o limite rígido do heap definido pela atualização, devido às novas configurações do AppData ou implícito pelas alterações de limite de memória do contêiner, for menor do que o que já está confirmado.

Globalização para aplicativos móveis

Os aplicativos móveis no iOS, tvOS e MacCatalyst podem aceitar um novo modo de globalização híbrido que usa um pacote de ICU mais leve. No modo híbrido, os dados de globalização são extraídos parcialmente do pacote ICU e parcialmente de chamadas para as APIs nativas. O modo híbrido atende a todas as localidades compatíveis com dispositivo móvel.

O modo híbrido é mais adequado para aplicativos que não funcionam no modo de globalização invariável e que usam culturas que foram cortadas dos dados de ICU em dispositivos móveis. Você também pode usá-lo quando quiser carregar um arquivo de dados de ICU menor. (O arquivo icudt_hybrid.dat é 34,5 % menor que o arquivo de dados de ICU padrão icudt.dat.)

Para usar o modo híbrido de globalização, defina a propriedade MSBuild HybridGlobalization como true:

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

Existem algumas limitações a serem consideradas, como:

  • Devido a limitações da API nativa, nem todas as APIs de globalização têm suporte no modo híbrido.
  • Algumas das APIs com suporte têm um comportamento diferente.

Para verificar se o aplicativo foi afetado, confira Diferenças comportamentais.

Interoperabilidade COM gerada pela origem

O .NET 8 inclui um novo gerador de origem que dá suporte à Interoperabilidade com interfaces COM. Você pode usar o GeneratedComInterfaceAttribute para marcar uma interface como uma interface COM para o gerador de origem. Em seguida, o gerador de origem gerará código para habilitar a chamada do código C# para o código não gerenciado. Ele também gera código para habilitar a chamada de código não gerenciado em C#. Esse gerador de origem se integra ao LibraryImportAttribute e você pode usar tipos com o GeneratedComInterfaceAttribute como parâmetros e tipos de retorno em métodos com atributos de 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);
}

O gerador de origem também dá suporte ao novo atributo GeneratedComClassAttribute para permitir que você passe tipos que implementam interfaces com o atributo GeneratedComInterfaceAttribute para código não gerenciado. O gerador de origem gerará o código necessário para expor um objeto COM que implementa as interfaces e encaminha chamadas para a implementação gerenciada.

Os métodos em interfaces com o atributo GeneratedComInterfaceAttribute dão suporte a todos os mesmos tipos que LibraryImportAttribute, e LibraryImportAttribute agora dá suporte a tipos com o atributo GeneratedComInterface e a tipos com o atributo GeneratedComClass.

Se o código C# usar apenas uma interface com GeneratedComInterface atribuído para encapsular um objeto COM de código não gerenciado ou encapsular um objeto gerenciado de C# para expor a código não gerenciado, você poderá usar as opções na propriedade Options para personalizar qual código será gerado. Essas opções significam que você não precisa escrever marshallers para cenários que você sabe que não serão usados.

O gerador de origem usa o novo tipo StrategyBasedComWrappers para criar e gerenciar os wrappers de objeto COM e os wrappers de objeto gerenciado. Esse novo tipo manipula o fornecimento da experiência de usuário esperada do .NET para a interoperabilidade COM, ao mesmo tempo em que fornece pontos de personalização para usuários avançados. Se seu aplicativo tiver seu próprio mecanismo para definir tipos de COM ou se você precisar dar suporte a cenários que o COM gerado pela origem atualmente não dá suporte, considere usar o novo tipo StrategyBasedComWrappers para adicionar os recursos ausentes para seu cenário e obter a mesma experiência de usuário do .NET para seus tipos COM.

Se você estiver usando o Visual Studio, novos analisadores e correções de código facilitam a conversão do código de interoperabilidade COM existente para usar a interoperabilidade gerada pela origem. Ao lado de cada interface que tem o ComImportAttribute, uma lâmpada oferece uma opção para converter em interoperabilidade gerada pela origem. A correção altera a interface para usar o atributo GeneratedComInterfaceAttribute. E ao lado de cada classe que implementa uma interface com GeneratedComInterfaceAttribute, uma lâmpada oferece uma opção para adicionar o atributo GeneratedComClassAttribute ao tipo . Depois que os tipos forem convertidos, você poderá mover seus métodos DllImport para usar o LibraryImportAttribute.

Limitações

O gerador de origem COM não dá suporte à afinidade de apartamento, usando a palavra-chave new para ativar um CoClass COM e as seguintes APIs:

Gerador de origem para configuração da associação

O .NET 8 apresenta um gerador de origem para fornecer AOT e configuração favorável ao corte no ASP.NET Core. O gerador é uma alternativa à implementação baseada em reflexão pré-existente.

O gerador investiga chamadas para Configure(TOptions), Bind e Get para recuperar informações de tipo. Quando o gerador é habilitado em um projeto, o compilador escolhe implicitamente os métodos gerados em vez das implementações de estrutura baseadas em reflexão pré-existentes.

Nenhuma alteração de código-fonte é necessária para usar o gerador. Ele é habilitado por padrão em aplicativos Web com AOT. Para outros tipos de projeto, o gerador de origem está desativado por padrão, mas você pode ativar a opção definindo a propriedade EnableConfigurationBindingGenerator como true no arquivo de projeto:

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

O código a seguir mostra um exemplo de invocação do associador.

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 principais do .NET

Esta seção contém os seguintes subtópicos:

Reflexão

Os ponteiros de função foram introduzidos no .NET 5. No entanto, o suporte correspondente para reflexão não foi adicionado naquele momento. Ao usar typeof ou reflexão em um ponteiro de função, por exemplo, typeof(delegate*<void>()) ou FieldInfo.FieldType respectivamente, um IntPtr foi retornado. Do .NET 8 em diante, um objeto System.Type é retornado. Esse tipo fornece acesso aos metadados do ponteiro de função, incluindo as convenções de chamada, o tipo de retorno e os parâmetros.

Observação

Uma instância de ponteiro de função, que é um endereço físico para uma função, continua a ser representada como um IntPtr. Somente o tipo de reflexão foi alterado.

Atualmente, a nova funcionalidade é implementada apenas no runtime do CoreCLR e no MetadataLoadContext.

Novas APIs foram adicionadas a System.Type, como IsFunctionPointer, e a System.Reflection.PropertyInfo, System.Reflection.FieldInfo e System.Reflection.ParameterInfo. O código a seguir mostra como usar algumas das novas APIs para reflexão.

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

O exemplo anterior produz a seguinte saída:

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

Serialização

Muitas melhorias foram feitas na System.Text.Json funcionalidade de serialização e desserialização no .NET 8. Por exemplo, você pode personalizar a manipulação de membros que não estão no conteúdo JSON.

As seções a seguir descrevem outras melhorias de serialização:

Para obter mais informações gerais sobre a serialização JSON, confira Serialização e desserialização JSON no .NET.

Suporte interno para tipos adicionais

O serializador tem suporte interno para os seguintes tipos adicionais.

  • Tipos numéricos Half, Int128 e UInt128.

    Console.WriteLine(JsonSerializer.Serialize(
        [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ]
    ));
    // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
    
  • Valores Memory<T> e ReadOnlyMemory<T>. Os valores byte são serializados para cadeias de caracteres Base64 e outros tipos para matrizes JSON.

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

Gerador de origem

O .NET 8 inclui aprimoramentos do gerador de origem System.Text.Json que visam tornar a experiência AOT nativa em par com o serializador baseado em reflexão. Por exemplo:

  • O gerador de origem agora dá suporte à serialização de tipos required e init propriedades. Ambos já tinham suporte na serialização baseada em reflexão.

  • Formatação aprimorada do código gerado pela origem.

  • Paridade de recursos JsonSourceGenerationOptionsAttribute com JsonSerializerOptions. Para obter mais informações, confira Especificar opções (gerador de origens).

  • Diagnósticos adicionais (como SYSLIB1034 e SYSLIB1039).

  • Não inclui tipos de propriedades ignoradas ou inacessíveis.

  • Suporte para aninhamento declarações JsonSerializerContext em tipos arbitrários.

  • Suporte para tipos gerados pelo compilador ou indesejáveis em cenários de geração de origem fracamente tipados. Como os tipos gerados pelo compilador não podem ser especificados explicitamente pelo gerador de origem, o System.Text.Json agora executa a resolução ancestral mais próxima em tempo de execução. Essa resolução determina o supertipo mais apropriado com o qual serializar o valor.

  • Novo tipo JsonStringEnumConverter<TEnum> de conversor . Não há suporte para a classe JsonStringEnumConverter existente no AOT nativo. Você pode anotar seus tipos de enumeração da seguinte maneira:

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

    Para obter mais informações, consulte Serializar campos de enumeração como cadeias de caracteres.

  • A nova JsonConverter.Type propriedade permite que você pesquise o tipo de uma instância não genérica JsonConverter:

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

    A propriedade é anulável, pois retorna null para instâncias JsonConverterFactory e typeof(T) para instâncias JsonConverter<T> .

Encadear geradores de origem

A classe JsonSerializerOptions inclui uma nova propriedade TypeInfoResolverChain que complementa a propriedade TypeInfoResolver existente. Essas propriedades são usadas na personalização de contrato para encadear geradores de origem. A adição da nova propriedade significa que você não precisa especificar todos os componentes encadeados em um site de chamada, eles podem ser adicionados após o fato. O TypeInfoResolverChain também permite que você introspecte a cadeia ou remova componentes dela. Para obter mais informações, confira Combinar geradores de origem.

Além disso, JsonSerializerOptions.AddContext<TContext>() agora está obsoleto. Ela foi substituída pelas propriedades TypeInfoResolver e TypeInfoResolverChain. Para obter mais informações, confira SYSLIB0049.

Hierarquias de interface

O .NET 8 adiciona suporte para serialização de propriedades de hierarquias de interface.

O código a seguir mostra um exemplo em que as propriedades da interface imediatamente implementada e da interface base são serializadas.

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

Políticas de nomenclatura

JsonNamingPolicy inclui novas políticas de nomenclatura para as conversões de nome de propriedade snake_case (com um sublinhado) e kebab-case (com hífen). Use essas políticas de modo semelhante à política JsonNamingPolicy.CamelCase:

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

Para obter mais informações, consulte Use uma política de nomenclatura interna.

Propriedades somente leitura

Agora você pode desserializar em campos ou propriedades somente leitura (ou seja, aqueles que não têm um acessador set).

Para aceitar esse suporte globalmente, defina uma nova opção, PreferredObjectCreationHandling, como JsonObjectCreationHandling.Populate. Se a compatibilidade for uma preocupação, você também poderá habilitar a funcionalidade de forma mais granular colocando o atributo [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] em tipos específicos cujas propriedades devem ser preenchidas ou em propriedades individuais.

Por exemplo, considere o código a seguir que desserializa em um tipo CustomerInfo que tem duas propriedades somente leitura.

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 do .NET 8, os valores de entrada eram ignorados e as propriedades Names e Company mantinham seus valores padrão.

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

Agora os valores de entrada são usados para preencher as propriedades somente leitura durante a desserialização.

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

Para obter mais informações sobre o comportamento de desserialização de popular, consulte Preencher propriedades inicializadas.

Desabilitar o padrão baseado em reflexão

Agora você pode desabilitar usando o serializador baseado em reflexão por padrão. Essa desabilitação é útil para evitar a raiz acidental de componentes de reflexão que nem estão em uso, especialmente em aplicativos AOT cortados e nativos. Para desabilitar a serialização padrão baseada em reflexão, exigindo que um argumento JsonSerializerOptions seja passado para os métodos de serialização e desserialização JsonSerializer, defina a propriedade JsonSerializerIsReflectionEnabledByDefault MSBuild para false no seu arquivo do projeto.

Use a nova API IsReflectionEnabledByDefault para verificar o valor da alternância do recurso. Se você for um autor de biblioteca com base no System.Text.Json, poderá contar com a propriedade para configurar seus padrões sem criar componentes de reflexão de raiz acidentalmente.

Para obter mais informações, consulte Desabilitar os padrões de reflexão.

Novos métodos de API JsonNode

Os tipos JsonNode e System.Text.Json.Nodes.JsonArray incluem os novos métodos a seguir.

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

Membros não públicos

Você pode optar por membros não públicos no contrato de serialização para um determinado tipo usando as anotações de atributo JsonIncludeAttribute e 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 obter mais informações, confira Usar tipos imutáveis e membros e acessadores não públicos.

APIs de desserialização de streaming

O .NET 8 inclui novos IAsyncEnumerable<T> métodos de extensão de desserialização de streaming, por exemplo GetFromJsonAsAsyncEnumerable. Existem métodos semelhantes que retornam Task<TResult>, por exemplo, HttpClientJsonExtensions.GetFromJsonAsync. Os novos métodos de extensão invocam APIs de streaming e retornam IAsyncEnumerable<T>.

O código a seguir mostra como você pode usar os novos métodos de extensão.

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 extensão WithAddedModifier

O novo método de extensão WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>) permite que você introduza facilmente modificações nos contratos de serialização de instâncias IJsonTypeInfoResolver arbitrárias.

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

New JsonContent.Create overloads

Agora você pode criar JsonContent instâncias usando contratos trim-safe ou gerados pela origem. Os novos métodos são:

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

Congelar uma instância de JsonSerializerOptions

Os novos métodos a seguir permitem controlar quando uma JsonSerializerOptions instância é congelada:

  • JsonSerializerOptions.MakeReadOnly()

    Essa sobrecarga foi projetada para ser trim-safe e, portanto, gerará uma exceção nos casos em que a instância de opções não foi configurada com um resolvedor.

  • JsonSerializerOptions.MakeReadOnly(Boolean)

    Se você passar true para essa sobrecarga, ela preencherá a instância de opções com o resolvedor de reflexão padrão se houver uma ausente. Esse método é marcado RequiresUnreferenceCode/RequiresDynamicCode e, portanto, é inadequado para aplicativos AOT nativos.

A nova IsReadOnly propriedade permite verificar se a instância de opções está congelada.

Abstração de tempo

A nova classe TimeProvider e a interface ITimer adicionam funcionalidade de abstração de tempo, o que permite simular o tempo em cenários de teste. Além disso, você pode usar a abstração de tempo para simular operações Task que dependem da progressão de tempo usando Task.Delay e Task.WaitAsync. A abstração de tempo dá suporte às seguintes operações de tempo essenciais:

  • Recuperar hora local e UTC
  • Obter um carimbo de data/hora para medir o desempenho
  • Criar um temporizador

O snippet de código a seguir mostra alguns exemplos 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);
}

Melhorias UTF8

Se você quiser habilitar a gravação de uma representação semelhante a uma cadeia de caracteres do seu tipo em um intervalo de destino, implemente a nova interface IUtf8SpanFormattable em seu tipo. Essa nova interface está intimamente relacionada a ISpanFormattable, mas tem como alvo UTF8 e Span<byte>, em vez de UTF16 e Span<char>.

O IUtf8SpanFormattable foi implementado em todos os tipos primitivos (mais outros), com exatamente a mesma lógica compartilhada, seja direcionando string, Span<char> ou Span<byte>. Ele tem suporte completo para todos os formatos (incluindo o novo especificador binário "B") e todas as culturas. Isso significa que você agora pode formatar diretamente para UTF8 de 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 e Version.

Novos métodos Utf8.TryWrite fornecem um equivalente baseado em UTF8 para os métodos existentes MemoryExtensions.TryWrite, que são baseados em UTF16. Você pode usar a sintaxe de cadeia de caracteres interpolada para formatar uma expressão complexa diretamente em um intervalo de bytes UTF8, por exemplo:

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

A implementação reconhece IUtf8SpanFormattable nos valores de formato e usa suas implementações para gravar suas representações UTF8 diretamente no intervalo de destino.

A implementação também utiliza o novo método Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32), que, juntamente com seu equivalente Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32), dá suporte à codificação e à decodificação em um intervalo de destino. Se o intervalo não for longo o suficiente para manter o estado resultante, os métodos retornarão false em vez de lançar uma exceção.

Métodos para trabalhar com aleatoriedade

Os tipos System.Random e System.Security.Cryptography.RandomNumberGenerator introduzem dois novos métodos para trabalhar com aleatoriedade.

GetItems<T>()

Os novos métodos System.Random.GetItems e System.Security.Cryptography.RandomNumberGenerator.GetItems permitem que você escolha aleatoriamente um número especificado de itens de um conjunto de entrada. O exemplo a seguir mostra como usar System.Random.GetItems<T>() (na instância fornecida pela propriedade Random.Shared) para inserir aleatoriamente 31 itens em uma matriz. Este exemplo pode ser usado em um jogo do tipo "Genius" em que os jogadores precisam se lembrar de uma sequência de botões coloridos.

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>()

Os novos métodos Random.Shuffle e RandomNumberGenerator.Shuffle<T>(Span<T>) permitem que você randomize a ordem de um intervalo. Esses métodos são úteis para reduzir o desvio de treinamento no machine learning (portanto, a primeira etapa nem sempre é o treinamento e a última é sempre um teste).

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 focados em desempenho

O .NET 8 apresenta vários novos tipos destinados a aprimorar o desempenho do aplicativo.

  • O novo namespace System.Collections.Frozen inclui os tipos de coleção FrozenDictionary<TKey,TValue> e FrozenSet<T>. Esses tipos não permitem alterações em chaves e valores depois que uma coleção é criada. Esse requisito permite operações de leitura mais rápidas (por exemplo, TryGetValue()). Esses tipos são particularmente úteis para coleções que são preenchidas no primeiro uso e persistidas durante a execução de um serviço de longa duração, por exemplo:

    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 procuram a primeira ocorrência de qualquer valor na coleção passada. O novo tipo System.Buffers.SearchValues<T> foi projetado para ser passado para esses métodos. De maneira correspondente, o .NET 8 adiciona novas sobrecargas de métodos como MemoryExtensions.IndexOfAny que aceitam uma instância do novo tipo. Quando você cria uma instância do SearchValues<T>, todos os dados necessários para otimizar pesquisas seguintes são derivados naquele momento, significando que o trabalho é feito antecipadamente.

  • O novo tipo System.Text.CompositeFormat é útil para otimizar cadeias de caracteres de formato que não são conhecidas no tempo de compilação (por exemplo, se a cadeia de caracteres de formato for carregada em um arquivo de recurso). Um pouco de tempo extra é gasto antecipadamente para ações como analisar a cadeia de caracteres, mas isso evita que o trabalho precise ser feito em 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);
    
  • Novos tipos System.IO.Hashing.XxHash3 e System.IO.Hashing.XxHash128 fornecem implementações dos algoritmos de hash XXH3 e XXH128 rápidos.

System.Numerics e System.Runtime.Intrinsics

Esta seção aborda melhorias nos namespaces System.Numerics e System.Runtime.Intrinsics.

  • Vector256<T>, Matrix3x2 e Matrix4x4 aprimoraram a aceleração de hardware no .NET 8. Por exemplo, Vector256<T> foi reimplementado para ter operações 2x Vector128<T> internas, sempre que possível. Isso permite a aceleração parcial de algumas funções quando Vector128.IsHardwareAccelerated == true, mas Vector256.IsHardwareAccelerated == false, por exemplo, no Arm64.
  • Os intrínsecos de hardware agora são anotados com o atributo ConstExpected. Isso garante que os usuários estejam cientes quando o hardware subjacente espera uma constante e, portanto, quando um valor não constante possa prejudicar inesperadamente o desempenho.
  • A API Lerp(TSelf, TSelf, TSelf)Lerp foi adicionada a IFloatingPointIeee754<TSelf> e, portanto, float (Single), double (Double) e Half. Essa API permite que uma interpolação linear entre dois valores seja executada de modo eficiente e correto.

Vector512 e AVX-512

O .NET Core 3.0 expandiu o suporte ao SIMD para incluir as APIs intrínsecas de hardware específicas da plataforma para x86/x64. O .NET 5 adicionou suporte para Arm64 e .NET 7 adicionados aos intrínsecos de hardware multiplataforma. O .NET 8 oferece suporte a SIMD apresentando Vector512<T> e dando suporte a instruções do Intel Advanced Vector Extensions 512 (AVX-512).

Especificamente, o .NET 8 inclui suporte para os seguintes principais recursos do AVX-512:

  • Operações de vetor de 512 bits
  • Mais 16 registros SIMD
  • Instruções adicionais disponíveis para vetores de 128 bits, 256 bits e 512 bits

Se você tiver hardware que dê suporte à funcionalidade, o Vector512.IsHardwareAccelerated agora relatará true.

O .NET 8 também adiciona várias classes específicas da plataforma no namespace System.Runtime.Intrinsics.X86:

Essas classes seguem a mesma forma geral que outras ISAs (arquiteturas de conjunto de instruções) na medida em que expõem uma propriedade IsSupported e uma classe aninhada Avx512F.X64 para obter instruções disponíveis apenas para processos de 64 bits. Além disso, cada classe tem uma classe aninhada Avx512F.VL que expõe as extensões Avx512VL (comprimento do vetor) para o conjunto de instruções correspondente.

Mesmo que você não use instruções específicas para Vector512 ou Avx512F em seu código, provavelmente ainda se beneficiará do novo suporte do AVX-512. O JIT pode aproveitar os registros e instruções adicionais implicitamente ao usar Vector128<T> ou Vector256<T>. A biblioteca de classes base usa esses intrínsecos de hardware internamente na maioria das operações expostas por Span<T> e ReadOnlySpan<T> em muitas das APIs matemáticas expostas para os tipos primitivos.

Validação de dados

O namespace System.ComponentModel.DataAnnotations inclui novos atributos de validação de dados destinados a cenários de validação em serviços nativos de nuvem. Embora os validadores pré-existentes DataAnnotations sejam voltados para a validação típica de entrada de dados da interface do usuário, como campos em um formulário, os novos atributos são projetados para validar dados de entrada não usuário, como opções de configuração. Além dos novos atributos, novas propriedades foram adicionadas aos tipos RangeAttribute e RequiredAttribute.

Nova API Descrição
RangeAttribute.MinimumIsExclusive
RangeAttribute.MaximumIsExclusive
Especifica se os limites estão incluídos no intervalo permitido.
System.ComponentModel.DataAnnotations.LengthAttribute Especifica os limites inferior e superior para cadeias de caracteres ou coleções. Por exemplo, [Length(10, 20)] requer pelo menos 10 elementos e no máximo 20 elementos em uma coleção.
System.ComponentModel.DataAnnotations.Base64StringAttribute Valida se uma cadeia de caracteres é uma representação base64 válida.
System.ComponentModel.DataAnnotations.AllowedValuesAttribute
System.ComponentModel.DataAnnotations.DeniedValuesAttribute
Especifique listas de permissões e listas de negação, respectivamente. Por exemplo, [AllowedValues("apple", "banana", "mango")].

Métricas

Novas APIs permitem anexar marcas de par chave-valor a objetos Meter e Instrument ao criá-las. Os agregadores de medidas de métrica publicadas podem usar as marcas para diferenciar os 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);

As novas APIs incluem o seguinte:

Criptografia

O .NET 8 adiciona suporte para os primitivos de hash SHA-3. (No momento, SHA-3 tem suporte do Linux com OpenSSL 1.1.1 ou posterior e Windows 11 Build 25324 ou posterior.) APIs em que SHA-2 está disponível agora oferecem um complemento SHA-3. Isso inclui SHA3_256, SHA3_384e SHA3_512 para hash; HMACSHA3_256, HMACSHA3_384e HMACSHA3_512 para HMAC; HashAlgorithmName.SHA3_256, HashAlgorithmName.SHA3_384e HashAlgorithmName.SHA3_512 para hash em que o algoritmo é configurável; e RSAEncryptionPadding.OaepSHA3_256, RSAEncryptionPadding.OaepSHA3_384e RSAEncryptionPadding.OaepSHA3_512 para criptografia OAEP RSA.

O exemplo a seguir mostra como usar as APIs, incluindo a propriedade SHA3_256.IsSupported para determinar se a plataforma dá suporte ao 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
{
    // ...
}

No momento, o suporte ao SHA-3 destina-se a dar suporte a primitivos criptográficos. Construções e protocolos de nível superior não devem dar inicialmente suporte total ao SHA-3. Esses protocolos incluem certificados X.509, SignedXml e COSE.

Rede

Suporte para proxy HTTPS

Até agora, os tipos de proxy que HttpClient suportavam todos permitiam que um "man-in-the-middle" visse a qual site o cliente está se conectando, mesmo para URIs HTTPS. HttpClient agora dá suporte ao proxy HTTPS, que cria um canal criptografado entre o cliente e o proxy para que todas as solicitações possam ser tratadas com privacidade total.

Para habilitar o proxy HTTPS, defina a all_proxy variável de ambiente ou use a classe WebProxy para controlar o proxy programaticamente.

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

Você também pode usar a classe WebProxy para controlar o proxy programaticamente.

Métodos ZipFile baseados em fluxo

O .NET 8 inclui novas sobrecargas de ZipFile.CreateFromDirectory que permitem coletar todos os arquivos incluídos em um diretório e compactá-los e, em seguida, armazenar o arquivo zip resultante no fluxo fornecido. Da mesma forma, novas sobrecargas de ZipFile.ExtractToDirectory permitem que você forneça um fluxo que contém um arquivo compactado e extraia seu conteúdo para o sistema de arquivos. Estas são as novas 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) { }
}

Essas novas APIs podem ser úteis quando o espaço em disco é restrito, pois evitam ter que usar o disco como uma etapa intermediária.

Bibliotecas de extensão

Esta seção contém os seguintes subtópicos:

Serviços de DI com chave

Os serviços de injeção de dependência (DI) com chave fornecem um meio de registrar e recuperar serviços de DI usando chaves. Ao usar chaves, você pode definir o escopo de como registrar e consumir serviços. Estas são algumas das novas APIs:

O exemplo a seguir mostra como usar os serviços de DI com chave.

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 obter mais informações, confira dotnet/runtime#64427.

Serviços de ciclo de vida hospedados

Os serviços hospedados agora têm mais opções para execução durante o ciclo de vida do aplicativo. IHostedService forneceu StartAsync e StopAsync, e agora IHostedLifecycleService fornece estes métodos adicionais:

Esses métodos são executados antes e depois dos pontos existentes, respectivamente.

O exemplo a seguir mostra como usar as novas APIs.

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 obter mais informações, confira dotnet/runtime#86511.

Validação de opções

Gerador de origem

Para reduzir a sobrecarga de inicialização e aprimorar o conjunto de recursos de validação, introduzimos um gerador de código-fonte que implementa a lógica de validação. O código a seguir mostra modelos de exemplo e classes de validador.

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

Se o aplicativo usar injeção de dependência, você poderá injetar a validação conforme mostrado no código de exemplo a seguir.

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

O .NET 8 apresenta o tipo ValidateOptionsResultBuilder para facilitar a criação de um objeto ValidateOptionsResult. É importante ressaltar que esse construtor permite o acúmulo de vários erros. Anteriormente, a criação do objeto ValidateOptionsResult necessário para implementar IValidateOptions<TOptions>.Validate(String, TOptions) era difícil e, às vezes, resultava em erros de validação em camadas. Se ocorressem vários erros, o processo de validação geralmente parava no primeiro erro.

O snippet de código a seguir mostra um exemplo 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();

Construtores LoggerMessageAttribute

LoggerMessageAttribute agora oferece sobrecargas de construtor adicionais. Anteriormente, era necessário escolher o construtor sem parâmetros ou o construtor que exigia todos os parâmetros (ID do evento, nível de log e mensagem). As novas sobrecargas oferecem maior flexibilidade na especificação dos parâmetros necessários com código reduzido. Se você não fornecer uma ID de evento, o sistema gerará uma automaticamente.

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

Métricas de extensões

Interface IMeterFactory

Você pode registrar a nova interface IMeterFactory em contêineres de DI (injeção de dependência) e usá-la para criar objetos Meter de maneira isolada.

Registre o IMeterFactory no contêiner de DI usando a implementação padrão da fábrica de medidores:

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

Em seguida, os consumidores podem obter a fábrica de medidores e usá-la para criar um objeto Meter.

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

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

Meter meter = meterFactory.Create(options);

Classe MetricCollector<T>

A nova classe MetricCollector<T> permite registrar medidas de métrica junto com carimbos de data/hora. Além disso, a classe oferece a flexibilidade de usar um provedor de tempo de sua escolha para uma geração precisa do carimbo de data/hora.

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

O pacote NuGet System.Numerics.Tensors atualizado inclui APIs no novo namespace TensorPrimitives que adicionam suporte para operações de tensor. Os primitivos tensoriais otimizam cargas de trabalho com uso intensivo de dados, como as de IA e aprendizado de máquina.

Cargas de trabalho de IA, como pesquisa semântica e RAG (geração aumentada por recuperação), estendem os recursos de linguagem natural de modelos de linguagem grandes, como ChatGPT, aumentando prompts com dados relevantes. Para essas cargas de trabalho, as operações em vetores, como similaridade de cosseno para encontrar os dados mais relevantes para responder a uma pergunta, são cruciais. O pacote System.Numerics.Tensors.TensorPrimitives fornece APIs para operações vetoriais, o que significa que você não precisa usar uma dependência externa ou escrever sua própria implementação.

Esse pacote substitui o pacote System.Numerics.Tensors.

Para obter mais informações, confira a postagem no blog Anúncio do .NET 8 RC 2.

Confira também