Condividi tramite


Novità del runtime di .NET 8

Questo articolo descrive le nuove funzionalità del runtime .NET per .NET 8.

Miglioramenti delle prestazioni

.NET 8 include miglioramenti alla generazione del codice e alla compilazione just-in time (JIT):

  • Miglioramenti delle prestazioni di Arm64
  • Miglioramenti di SIMD
  • Supporto per le estensioni ISA AVX-512 (vedere Vector512 e AVX-512)
  • Miglioramenti nativi del cloud
  • Miglioramenti della velocità effettiva JIT
  • Ottimizzazioni dei cicli e generali
  • Accesso ottimizzato per i campi contrassegnati con ThreadStaticAttribute
  • Allocazione di registri consecutivi. Arm64 include due istruzioni per la ricerca di vettori di tabella, che richiedono che tutte le entità nei rispettivi operandi tupla siano presenti in registri consecutivi.
  • JIT/AOT nativo può ora annullare la registrazione e vettorizzare automaticamente alcune operazioni di memoria con SIMD, ad esempio confronto, copia e azzeramento, se può determinare le dimensioni in fase di compilazione.

Inoltre, l'ottimizzazione PGO (Profile Guided Optimization) dinamica è stata migliorata ed è ora abilitata per impostazione predefinita. Non è più necessario usare un'opzione di configurazione di runtime per abilitarla. L'ottimizzazione PGO dinamica interagisce con la compilazione a livelli per ottimizzare ulteriormente il codice in base a strumentazione aggiuntiva attivata durante il livello 0.

In media, l'ottimizzazione PGO dinamica aumenta le prestazioni di circa il 15%. In un gruppo di test di benchmark di circa 4600 test, il 23% ha registrato miglioramenti delle prestazioni del 20% o più.

Promozione dello struct codegen

.NET 8 include un nuovo passaggio di ottimizzazione della promozione fisica per codegen che generalizza la capacità di JIT di alzare di livello le variabili struct. Questa ottimizzazione (chiamata anche sostituzione scalare delle aggregazioni) sostituisce i campi delle variabili struct con variabili primitive che JIT è quindi in grado di valutare e ottimizzare con maggiore precisione.

JIT supportava già questa ottimizzazione, ma con diverse limitazioni di grandi entità, tra cui:

  • L'ottimizzazione era supportata solo per gli struct con quattro campi o meno.
  • L'ottimizzazione era supportata solo se ogni campo era un tipo primitivo o un semplice struct che eseguiva il wrapping di un tipo primitivo.

La promozione fisica rimuove queste limitazioni, risolvendo così una serie di problemi JIT di lunga durata.

Garbage Collection

.NET 8 aggiunge una funzionalità per regolare il limite di memoria in tempo reale. Ciò è utile negli scenari dei servizi cloud, in cui la domanda è variabile. Per essere convenienti, i servizi devono aumentare e ridurre il consumo delle risorse in base alla fluttuazione della domanda. Quando un servizio rileva una riduzione della domanda, può ridurre il consumo di risorse abbassando il limite di memoria. In precedenza, ciò non sarebbe stato possibile perché il Garbage Collector (GC) non era a conoscenza della modifica e avrebbe potuto allocare più memoria rispetto al nuovo limite. Con questa modifica, è possibile chiamare l'API RefreshMemoryLimit() per aggiornare GC con il nuovo limite di memoria.

Esistono alcune limitazioni di cui tenere conto:

  • Nelle piattaforme a 32 bit (ad esempio, Windows x86 e Linux ARM), .NET non è in grado di stabilire un nuovo limite fisso dell'heap se non ne esiste già uno.
  • L'API potrebbe restituire un codice di stato diverso da zero che indica che l'aggiornamento non è riuscito. Questo può accadere se la riduzione è troppo aggressiva e non lascia spazio per gli interventi del GC. In questo caso, provare a chiamare GC.Collect(2, GCCollectionMode.Aggressive) per ridurre l'utilizzo corrente della memoria e quindi riprovare.
  • Se si aumenta il limite di memoria oltre le dimensioni che GC ritiene che il processo possa gestire durante l'avvio, la chiamata RefreshMemoryLimit avrà esito positivo, ma non sarà in grado di usare più memoria di quella percepita come limite.

Il frammento di codice seguente illustra come chiamare l'API.

GC.RefreshMemoryLimit();

È anche possibile aggiornare alcune delle impostazioni di configurazione di GC correlate al limite di memoria. Il frammento di codice seguente imposta il limite fisso dell'heap su 100 mebibyte (MiB):

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

L'API può generare un'eccezione InvalidOperationException se il limite fisso non è valido, ad esempio nel caso di percentuali negative del limite fisso dell'heap e se il limite fisso è troppo basso. Ciò può verificarsi se il limite fisso dell'heap impostato dall'aggiornamento, a causa delle nuove impostazioni AppData o in modo implicito in seguito alle modifiche del limite di memoria del contenitore, è inferiore a quello già confermato.

Globalizzazione per le app per dispositivi mobili

Le app per dispositivi mobili su iOS, tvOS e MacCatalyst possono acconsentire esplicitamente a una nuova modalità di globalizzazione ibrida che usa un pacchetto ICU più leggero. In modalità ibrida, i dati di globalizzazione vengono parzialmente recuperati dal pacchetto ICU e parzialmente dalle chiamate alle API native. La modalità ibrida può essere gestita in tutte le impostazioni locali supportate dai dispositivi mobili.

La modalità ibrida è più adatta per le app che non possono funzionare in modalità di globalizzazione invariante e che usano impostazioni cultura tagliate dai dati di ICU su dispositivi mobili. È anche possibile usarla quando si vuole caricare un file di dati ICU più piccolo. (Il file icudt_hybrid.dat è più piccolo del 34,5% rispetto al file di dati ICU predefinito icudt.dat.)

Per usare la modalità di globalizzazione ibrida, imposta la proprietà MSBuild HybridGlobalization su true:

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

Esistono alcune limitazioni da tenere presenti:

  • A causa delle limitazioni dell'API nativa, non tutte le API di globalizzazione sono supportate in modalità ibrida.
  • Alcune delle API supportate hanno un comportamento diverso.

Per verificare se l'applicazione è interessata, vedere Differenze comportamentali.

Interoperabilità COM generata dall'origine

.NET 8 include un nuovo generatore di origini che supporta l'interoperabilità con le interfacce COM. È possibile utilizzare GeneratedComInterfaceAttribute per contrassegnare un'interfaccia come interfaccia COM per il generatore di origini. Il generatore di origini genererà quindi il codice per abilitare la chiamata dal codice C# al codice non gestito. Genera anche il codice per abilitare la chiamata da codice non gestito in C#. Questo generatore di origini si integra con LibraryImportAttribute ed è possibile usare i tipi con GeneratedComInterfaceAttribute come parametri e tipi restituiti nei metodi con attributi 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);
}

Il generatore di origini supporta anche il nuovo attributo GeneratedComClassAttribute, per consentire di passare tipi che implementano interfacce con l'attributo GeneratedComInterfaceAttribute al codice non gestito. Il generatore di origini genererà il codice necessario per esporre un oggetto COM che implementa le interfacce e inoltra le chiamate all'implementazione gestita.

I metodi sulle interfacce con l'attributo GeneratedComInterfaceAttribute supportano tutti gli stessi tipi di LibraryImportAttribute e LibraryImportAttribute ora supporta tipi con attributi GeneratedComInterface e GeneratedComClass.

Se il codice C# usa solo un'interfaccia con attributi GeneratedComInterface per eseguire il wrapping di un oggetto COM da codice non gestito o eseguire il wrapping di un oggetto gestito da C# per esporre al codice non gestito, è possibile usare le opzioni nella proprietà Options per personalizzare il codice che verrà generato. Queste opzioni indicano che non è necessario scrivere marshaller per scenari che non verranno usati.

Il generatore di origini usa il nuovo tipo StrategyBasedComWrappers per creare e gestire i wrapper di oggetti COM e i wrapper dell'oggetto gestito. Questo nuovo tipo gestisce l'esperienza utente .NET prevista per l'interoperabilità COM, fornendo al tempo stesso punti di personalizzazione per gli utenti avanzati. Se l'applicazione ha un proprio meccanismo per la definizione dei tipi di COM o se è necessario supportare scenari non supportati da COM generati dall'origine, valutare la possibilità di usare il nuovo tipo StrategyBasedComWrappers per aggiungere le funzionalità mancanti allo scenario e ottenere la stessa esperienza utente .NET per i tipi COM.

Se si usa Visual Studio, i nuovi analizzatori e gli aggiornamenti del codice semplificano la conversione del codice di interoperabilità COM esistente per l'uso dell'interoperabilità generata dall'origine. Accanto a ogni interfaccia con ComImportAttribute, una lampadina offre l'opzione per la conversione in interoperabilità generata dall'origine. L’aggiornamento modifica l'interfaccia in modo da usare l'attributo GeneratedComInterfaceAttribute. Accanto a ogni classe che implementa un'interfaccia con GeneratedComInterfaceAttribute, una lampadina offre l'opzione per l’aggiunta dell'attributo GeneratedComClassAttribute al tipo. Dopo aver convertito i tipi, è possibile spostare i metodi DllImport per usare LibraryImportAttribute.

Limiti

Il generatore di origini COM non supporta l'affinità apartment, usando la parola chiave new per attivare una coclasse COM e le API seguenti:

Generatore di origini con associazione della configurazione

.NET 8 introduce un generatore di origini per fornire una configurazione AOT e con il supporto del trimming in ASP.NET Core. Il generatore è un'alternativa all'implementazione preesistente basata sulla reflection.

Il generatore di origine esegue il probe per le chiamate Configure(TOptions), Bind e Get per il recupero di informazioni sui tipi. Quando il generatore è abilitato in un progetto, il compilatore sceglie in modo implicito i metodi generati rispetto alle implementazioni preesistenti basate su reflection.

Non sono necessarie modifiche al codice sorgente per usare il generatore. È abilitato per impostazione predefinita nelle app Web AOT. Per altri tipi di progetto, il generatore di origini è disattivato per impostazione predefinita, ma è possibile acconsentire esplicitamente impostando la proprietà EnableConfigurationBindingGenerator su true nel file di progetto:

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

Il codice seguente mostra un esempio di chiamata del binder.

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

Principali librerie .NET

Questa sezione contiene i seguenti argomenti secondari:

Riflessione

I puntatori a funzione sono stati introdotti in .NET 5, anche se allora non è stato aggiunto il supporto corrispondente per la reflection. Quando si usa typeof o la reflection su un puntatore a funzione, ad esempio, rispettivamente typeof(delegate*<void>()) o FieldInfo.FieldType, viene restituito un IntPtr. A partire da .NET 8, viene restituito invece un oggetto System.Type. Questo tipo fornisce l'accesso ai metadati del puntatore a funzione, incluse le convenzioni di chiamata, il tipo restituito e i parametri.

Nota

Un'istanza del puntatore a funzione, ovvero un indirizzo fisico a una funzione, continua a essere rappresentata come IntPtr. Solo il tipo di reflection è cambiato.

La nuova funzionalità è attualmente implementata solo nel runtime CoreCLR e in MetadataLoadContext.

Sono state aggiunte nuove API a System.Type, ad esempio IsFunctionPointer, e a System.Reflection.PropertyInfo, System.Reflection.FieldInfo e System.Reflection.ParameterInfo. Il codice seguente illustra come usare alcune delle nuove API per la reflection.

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

L'esempio precedente produce l'output seguente:

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

Serializzazione

Sono stati apportati molti miglioramenti alle funzionalità di serializzazione e deserializzazione System.Text.Json in .NET 8. Ad esempio, è possibile personalizzare la gestione dei membri non inclusi nel payload JSON.

Le sezioni seguenti descrivono altri miglioramenti della serializzazione:

Per altre informazioni sulla serializzazione JSON in generale, vedere Serializzazione e deserializzazione JSON in .NET.

Supporto predefinito per tipi aggiuntivi

Il serializzatore include il supporto predefinito per i tipi aggiuntivi seguenti.

  • Tipi numerici Half, Int128 e UInt128.

    Console.WriteLine(JsonSerializer.Serialize(
        [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ]
    ));
    // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
    
  • Valori Memory<T> e ReadOnlyMemory<T>. I valori byte vengono serializzati in stringhe Base64 e altri tipi in matrici JSON.

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

Generatore di origini

.NET 8 include miglioramenti del generatore di origini System.Text.Json che hanno lo scopo di rendere l'esperienza per AOT nativo coerente con il serializzatore basato sulla reflection. Ad esempio:

  • Il generatore di origini supporta ora la serializzazione dei tipi con le proprietà required e init. Entrambe erano già supportate nella serializzazione basata su reflection.

  • Miglioramento della formattazione del codice generato dall'origine.

  • Parità delle funzionalità JsonSourceGenerationOptionsAttribute con JsonSerializerOptions. Per altre informazioni, vedere Specificare le opzioni (generazione di origini).

  • Diagnostica aggiuntiva, ad esempio SYSLIB1034 e SYSLIB1039.

  • Non vengono inclusi tipi di proprietà ignorate o inaccessibili.

  • Supporto per le dichiarazioni JsonSerializerContext di annidamento all'interno di tipi arbitrari.

  • Supporto per i tipi generati dal compilatore o anonimi in scenari di generazione di origini con tipizzazione debole. Poiché i tipi generati dal compilatore non possono essere specificati in modo esplicito dal generatore di origini, System.Text.Json ora esegue la risoluzione basata sul predecessore più vicino in fase di esecuzione. Questa risoluzione determina il supertipo più appropriato con cui serializzare il valore.

  • Nuovo tipo di convertitore JsonStringEnumConverter<TEnum>. La classe esistente JsonStringEnumConverter non è supportata in AOT nativo. È possibile annotare i tipi enumerazione come indicato di seguito:

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

    Per altre informazioni, vedere Serializzare i campi enumerazione come stringhe.

  • La nuova proprietà JsonConverter.Type consente di cercare il tipo di un'istanza JsonConverter non generica:

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

    La proprietà è nullable perché restituisce null per le istanze di JsonConverterFactory e typeof(T) per le istanze di JsonConverter<T>.

Concatenare i generatori di origini

La classe JsonSerializerOptions include una nuova proprietà TypeInfoResolverChain che integra la proprietà esistente TypeInfoResolver. Queste proprietà vengono usate nella personalizzazione del contratto per il concatenamento dei generatori di origini. L'aggiunta della nuova proprietà significa che non è necessario specificare tutti i componenti concatenati in un sito di chiamata, ma che possono essere aggiunti dopo il fatto. TypeInfoResolverChain consente inoltre di analizzare la catena o rimuovere componenti. Per altre informazioni, vedere Combinare generatori di origini.

Inoltre, il metodo JsonSerializerOptions.AddContext<TContext>() è ora obsoleto. È stato sostituito dalle proprietà TypeInfoResolver e TypeInfoResolverChain. Per altre informazioni, vedere SYSLIB0049.

Gerarchie di interfaccia

.NET 8 aggiunge il supporto per la serializzazione delle proprietà dalle gerarchie di interfaccia.

Il codice seguente mostra un esempio in cui vengono serializzate le proprietà sia dall'interfaccia implementata immediatamente sia dalla relativa interfaccia di 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; }
}

Criteri di denominazione

JsonNamingPolicy include nuovi criteri di denominazione per le conversioni dei nomi di proprietà snake_case (con un carattere di sottolineatura) e kebab-case (con un trattino). Usare questi criteri in modo analogo ai criteri esistenti JsonNamingPolicy.CamelCase:

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

Per altre informazioni, vedere Usare un criterio di denominazione predefinito.

Proprietà di sola lettura

È ora possibile eseguire la deserializzazione per campi o proprietà di sola lettura, ovvero quelli che non hanno una funzione di accesso set.

Per acconsentire esplicitamente a questo supporto a livello globale, impostare la nuova opzione PreferredObjectCreationHandling su JsonObjectCreationHandling.Populate. Se la compatibilità è un problema, è anche possibile abilitare la funzionalità in modo più granulare inserendo l'attributo [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] per tipi specifici per cui devono essere popolate le proprietà o per singole proprietà.

Si consideri, ad esempio, il codice seguente che esegue la deserializzazione in un tipo CustomerInfo con due proprietà di sola lettura.

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

Prima di .NET 8, i valori di input venivano ignorati e le proprietà Names e Company mantenevano i valori predefiniti.

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

I valori di input vengono ora usati per popolare le proprietà di sola lettura durante la deserializzazione.

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

Per altre informazioni sul comportamento di deserializzazione di popolamento, vedere Popolare le proprietà inizializzate.

Disabilitare l'impostazione predefinita basata sulla reflection

È ora possibile disabilitare l'uso del serializzatore basato su reflection per impostazione predefinita. Questa disabilitazione è utile per evitare il rooting accidentale dei componenti di reflection che non sono nemmeno in uso, in particolare nelle app con trimming e AOT nativo. Per disabilitare la serializzazione predefinita basata su reflection richiedendo il passaggio di un argomentoJsonSerializerOptions ai metodi di serializzazione e deserializzazione JsonSerializer, impostare la proprietà MSBuild JsonSerializerIsReflectionEnabledByDefault su false nel file di progetto.

Usare la nuova API IsReflectionEnabledByDefault per controllare il valore dell'opzione della funzionalità. Gli autori di librerie basate su System.Text.Json possono fare affidamento sulla proprietà per configurare le impostazioni predefinite senza eseguire accidentalmente il rooting dei componenti di reflection.

Per altre informazioni, vedere Disabilitare le impostazioni predefinite di reflection.

Nuovi metodi dell'API JsonNode

I tipi JsonNode e System.Text.Json.Nodes.JsonArray includono i nuovi metodi seguenti.

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

Membri non pubblici

È possibile acconsentire esplicitamente ai membri non pubblici nel contratto di serializzazione per un determinato tipo usando le annotazioni degli attributi 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; }
}

Per altre informazioni, vedere Usare tipi non modificabili e membri e funzioni di accesso non pubblici.

API di deserializzazione di streaming

.NET 8 include nuovi metodi di estensione della deserializzazione di streaming IAsyncEnumerable<T>, ad esempio GetFromJsonAsAsyncEnumerable. Esistono metodi simili che restituiscono Task<TResult>, ad esempio HttpClientJsonExtensions.GetFromJsonAsync. I nuovi metodi di estensione richiamano le API di streaming e restituiscono IAsyncEnumerable<T>.

Il codice seguente illustra un possibile uso dei nuovi metodi di estensione.

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

Metodo di estensione WithAddedModifier

Il nuovo metodo di estensione WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>) consente di introdurre facilmente modifiche ai contratti di serializzazione di istanze di IJsonTypeInfoResolver arbitrarie.

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

Nuovi overload JsonContent.Create

È ora possibile creare istanze di JsonContent usando contratti che supportano il trimming o generati dall'origine. I nuovi metodi sono:

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

Bloccare un'istanza di JsonSerializerOptions

I nuovi metodi seguenti consentono di controllare quando un'istanza di JsonSerializerOptions è bloccata:

  • JsonSerializerOptions.MakeReadOnly()

    Questo overload è progettato per supportare il trimming e genererà quindi un'eccezione nei casi in cui l'istanza delle opzioni non è stata configurata con un resolver.

  • JsonSerializerOptions.MakeReadOnly(Boolean)

    Se si passa true a questo overload, l'istanza delle opzioni viene popolata con il resolver di reflection predefinito, se mancante. Questo metodo è contrassegnato RequiresUnreferenceCode/RequiresDynamicCode e pertanto non è adatto per le applicazioni AOT nativo.

La nuova proprietà IsReadOnly consente di controllare se l'istanza delle opzioni è bloccata.

Astrazione temporale

La nuova classe TimeProvider e l'interfaccia ITimer aggiungono la funzionalità di astrazione temporale, che consente di simulare il tempo negli scenari di test. Inoltre, è possibile usare l'astrazione temporale per simulare operazioni Task che si basano sulla progressione temporale usando Task.Delay e Task.WaitAsync. L'astrazione temporale supporta le operazioni temporali essenziali seguenti:

  • Recuperare l'ora locale e UTC
  • Ottenere un timestamp per misurare le prestazioni
  • Creare un timer

Il frammento di codice seguente mostra alcuni esempi di utilizzo.

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

Miglioramenti di UTF8

Se si vuole abilitare la scrittura di una rappresentazione di tipo stringa del tipo in un intervallo di destinazione, implementare la nuova interfaccia IUtf8SpanFormattable per il tipo. Questa nuova interfaccia è strettamente correlata a ISpanFormattable, ma è destinata a UTF8 e Span<byte> invece di UTF16 e Span<char>.

L'interfaccia IUtf8SpanFormattable è stata implementata in tutti i tipi primitivi (più altri), con la stessa logica condivisa valida per la destinazione string, Span<char> o Span<byte>. Include il supporto completo per tutti i formati (incluso il nuovo identificatore binario "B") e tutte le impostazioni cultura. Ciò significa che è ora possibile applicare direttamente la formattazione UTF8 da Byte, Complex, DateTime UInt32 Half Guid Double Decimal IPAddress DateTimeOffset DateOnly Char IPNetwork Int16 SByte NFloat Single IntPtr Rune Int128 TimeOnly TimeSpan Int64 Int32 UInt64 UInt128 UInt16 UIntPtr e Version.

I nuovi metodi Utf8.TryWrite forniscono una controparte basata su UTF8 per i metodi MemoryExtensions.TryWrite esistenti, basati su UTF16. È possibile usare la sintassi di stringa interpolata per formattare un'espressione complessa direttamente in un intervallo di byte UTF8, ad esempio:

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

L'implementazione riconosce IUtf8SpanFormattable nei valori di formato e usa le relative implementazioni per scrivere le rappresentazioni UTF8 direttamente nell'intervallo di destinazione.

L'implementazione usa anche il nuovo metodo Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32) che, insieme alla controparte Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32), supporta la codifica e la decodifica in un intervallo di destinazione. Se la lunghezza dell'intervallo non è sufficiente per contenere lo stato risultante, i metodi restituiscono false anziché generare un'eccezione.

Metodi per l'uso della casualità

I tipi System.Random e System.Security.Cryptography.RandomNumberGenerator introducono due nuovi metodi per l'uso della casualità.

GetItems<T>()

I nuovi metodi System.Random.GetItems e System.Security.Cryptography.RandomNumberGenerator.GetItems consentono di scegliere in modo casuale un numero specificato di elementi da un set di input. Nell'esempio seguente viene illustrato come usare System.Random.GetItems<T>() (nell'istanza fornita dalla proprietà Random.Shared) per inserire in modo casuale 31 elementi in una matrice. Questo esempio potrebbe essere usato in un gioco tipo "Simon" in cui i giocatori devono ricordare una sequenza di pulsanti colorati.

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

I nuovi metodi Random.Shuffle e RandomNumberGenerator.Shuffle<T>(Span<T>) consentono di rendere casuale l'ordine di un intervallo. Questi metodi sono utili per ridurre la distorsione del training nel Machine Learning, in modo che la prima operazione non sia sempre il training e l'ultima non siano sempre i test.

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

Tipi incentrati sulle prestazioni

.NET 8 introduce diversi nuovi tipi progettati per migliorare le prestazioni delle app.

  • Il nuovo spazio dei nomi System.Collections.Frozen include i tipi di raccolta FrozenDictionary<TKey,TValue> e FrozenSet<T>. Questi tipi non consentono modifiche alle chiavi e ai valori dopo la creazione di una raccolta. Questo requisito consente operazioni di lettura più veloci, ad esempio TryGetValue(). Questi tipi sono particolarmente utili per le raccolte popolate al primo utilizzo e quindi rese persistenti per la durata di un servizio di lunga durata, ad esempio:

    private static readonly FrozenDictionary<string, bool> s_configurationData =
        LoadConfigurationData().ToFrozenDictionary(optimizeForReads: true);
    
    // ...
    if (s_configurationData.TryGetValue(key, out bool setting) && setting)
    {
        Process();
    }
    
  • Metodi come MemoryExtensions.IndexOfAny cercano la prima occorrenza di qualsiasi valore nella raccolta passata. Il nuovo tipo System.Buffers.SearchValues<T> è progettato per essere passato a tali metodi. In modo corrispondente, .NET 8 aggiunge nuovi overload di metodi come MemoryExtensions.IndexOfAny che accettano un'istanza del nuovo tipo. Quando si crea un'istanza di SearchValues<T>, tutti i dati necessari per ottimizzare le ricerche successive vengono derivati in quel momento, ovvero il lavoro viene eseguito in anticipo.

  • Il nuovo tipo System.Text.CompositeFormat è utile per ottimizzare le stringhe di formato non note in fase di compilazione (ad esempio, se la stringa di formato viene caricata da un file di risorse). Viene impiegato un po' di tempo aggiuntivo all'inizio per eseguire operazioni come l'analisi della stringa, ma si evitano così ripetizioni per ogni 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);
    
  • I nuovi tipi System.IO.Hashing.XxHash3 e System.IO.Hashing.XxHash128 forniscono implementazioni degli algoritmi hash XXH3 e XXH128 veloci.

System.Numerics e System.Runtime.Intrinsics

Questa sezione illustra i miglioramenti apportati agli spazi dei nomi System.Numerics e System.Runtime.Intrinsics.

  • Vector256<T>, Matrix3x2 e Matrix4x4 hanno migliorato l'accelerazione hardware in .NET 8. Ad esempio, Vector256<T> è stato reimplementato per eseguire internamente operazioni 2x Vector128<T>, ove possibile. Ciò consente l'accelerazione parziale di alcune funzioni quando Vector128.IsHardwareAccelerated == true ma Vector256.IsHardwareAccelerated == false, ad esempio in Arm64.
  • Gli intrinseci hardware sono ora annotati con l'attributo ConstExpected. Ciò garantisce che gli utenti sappiano quando l'hardware sottostante prevede una costante e pertanto quando un valore non costante potrebbe influire in modo imprevisto negativamente sulle prestazioni.
  • L'API Lerp(TSelf, TSelf, TSelf)Lerp è stata aggiunta a IFloatingPointIeee754<TSelf> e quindi a float (Single), double (Double) e Half. Questa API consente di eseguire un'interpolazione lineare tra due valori in modo efficiente e corretto.

Vector512 e AVX-512

In .NET Core 3.0 è stato esteso il supporto SIMD per includere le API intrinseche hardware specifiche della piattaforma per x86/x64. .NET 5 ha aggiunto il supporto per Arm64 e .NET 7 ha aggiunto gli intrinseci hardware multipiattaforma. .NET 8 offre ulteriore supporto per SIMD introducendo Vector512<T> e il supporto per istruzioni Intel Advanced Vector Extensions 512 (AVX-512).

In particolare, .NET 8 include il supporto per le funzionalità chiave seguenti di AVX-512:

  • Operazioni vettoriali a 512 bit
  • Altri 16 registri SIMD
  • Istruzioni aggiuntive disponibili per vettori a 128 bit, 256 bit e 512 bit

Se l'hardware disponibile supporta la funzionalità, ora Vector512.IsHardwareAccelerated restituisce true.

.NET 8 aggiunge anche diverse classi specifiche della piattaforma nello spazio dei nomi System.Runtime.Intrinsics.X86:

Queste classi seguono la stessa forma generale di altre architetture del set di istruzioni (ISA) in quanto espongono una proprietà IsSupported e una classe Avx512F.X64 annidata per istruzioni disponibili solo per i processi a 64 bit. Inoltre, ogni classe ha una classe Avx512F.VL annidata che espone le estensioni Avx512VL (lunghezza vettoriale) per il set di istruzioni corrispondente.

Anche se non si usano in modo esplicito istruzioni specifiche per Vector512 e per Avx512F nel codice, è probabile che il nuovo supporto di AVX-512 offra comunque vantaggi. JIT può sfruttare i registri e le istruzioni aggiuntivi in modo implicito quando si usa Vector128<T> o Vector256<T>. La libreria di classi di base usa questi intrinseci hardware internamente nella maggior parte delle operazioni esposte da Span<T> e ReadOnlySpan<T> e in molte API matematiche esposte per i tipi primitivi.

Convalida dei dati

Lo spazio dei nomi System.ComponentModel.DataAnnotations include nuovi attributi di convalida dei dati destinati agli scenari di convalida nei servizi nativi del cloud. Mentre i validator DataAnnotations preesistenti sono orientati alla tipica convalida dell'immissione dei dati dell'interfaccia utente, ad esempio i campi in un modulo, i nuovi attributi sono progettati per convalidare i dati non immessi dagli utenti, come le opzioni di configurazione. Oltre ai nuovi attributi, sono state aggiunte nuove proprietà ai tipi RangeAttribute e RequiredAttribute.

Nuova API Descrizione
RangeAttribute.MinimumIsExclusive
RangeAttribute.MaximumIsExclusive
Specifica se i limiti sono inclusi nell'intervallo consentito.
System.ComponentModel.DataAnnotations.LengthAttribute Specifica entrambi i limiti inferiore e superiore per stringhe o raccolte. Ad esempio, [Length(10, 20)] richiede minimo 10 elementi e al massimo 20 elementi in una raccolta.
System.ComponentModel.DataAnnotations.Base64StringAttribute Convalida che una stringa sia una rappresentazione Base64 valida.
System.ComponentModel.DataAnnotations.AllowedValuesAttribute
System.ComponentModel.DataAnnotations.DeniedValuesAttribute
Specificano rispettivamente gli elenchi di valori consentiti e non consenti. Ad esempio: [AllowedValues("apple", "banana", "mango")].

Metrica

Le nuove API consentono di associare tag di coppie chiave-valore agli oggetti Meter e Instrument quando vengono creati. Gli aggregatori delle misurazioni delle metriche pubblicate possono usare i tag per differenziare i valori aggregati.

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

Le nuove API includono:

Crittografia

.NET 8 aggiunge il supporto per le primitive di hashing SHA-3. SHA-3 è attualmente supportato da Linux con OpenSSL 1.1.1 o versione successiva e Windows 11 Build 25324 o versione successiva. Le API in cui è disponibile SHA-2 offrono ora anche SHA-3. Sono inclusi SHA3_256, SHA3_384 e SHA3_512 per l'hashing; HMACSHA3_256, HMACSHA3_384 e HMACSHA3_512 per HMAC; HashAlgorithmName.SHA3_256, HashAlgorithmName.SHA3_384 e HashAlgorithmName.SHA3_512 per l'hashing in cui l'algoritmo è configurabile; e RSAEncryptionPadding.OaepSHA3_256, RSAEncryptionPadding.OaepSHA3_384 e RSAEncryptionPadding.OaepSHA3_512 per la crittografia OAEP RSA.

L'esempio seguente illustra come usare le API, inclusa la proprietà SHA3_256.IsSupported per determinare se la piattaforma supporta 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
{
    // ...
}

Il supporto di SHA-3 è attualmente destinato al supporto delle primitive crittografiche. Le costruzioni e i protocolli di livello superiore non dovrebbero supportare completamente SHA-3 inizialmente. Questi protocolli includono certificati X.509, SignedXml e COSE.

Rete

Supporto per il proxy HTTPS

Fino ad ora, i tipi di proxy supportati da HttpClientconsentivano tutti a un "man-in-the-middle" di vedere il sito a cui si connette il client, anche per gli URI HTTPS. HttpClient ora supporta il proxy HTTPS, che crea un canale crittografato tra il client e il proxy in modo che tutte le richieste possano essere gestite con privacy completa.

Per abilitare il proxy HTTPS, impostare la variabile di ambiente all_proxy oppure usare la classe WebProxy per controllare il proxy a livello di codice.

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

È anche possibile usare la classe WebProxy per controllare il proxy a livello di codice.

Metodi ZipFile basati su flusso

.NET 8 include nuovi overload di ZipFile.CreateFromDirectory che consentono di raccogliere tutti i file inclusi in una directory e comprimerli, quindi archiviare il file ZIP risultante nel flusso specificato. In modo analogo, i nuovi overload di ZipFile.ExtractToDirectory consentono di fornire un flusso contenente un file compresso e di estrarne il contenuto nel file system. Questi sono i nuovi overload:

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

Queste nuove API possono essere utili quando lo spazio su disco è vincolato, perché evitano di dover usare il disco come passaggio intermedio.

Librerie di estensioni

Questa sezione contiene i seguenti argomenti secondari:

Servizi di inserimento delle dipendenze con chiave

I servizi di inserimento delle dipendenze con chiave forniscono un mezzo per la registrazione e il recupero di servizi di inserimento delle dipendenze tramite chiavi. Usando le chiavi, è possibile definire l'ambito della registrazione e dell'utilizzo dei servizi. Ecco alcune delle nuove API:

L'esempio seguente illustra come usare i servizi di inserimento delle dipendenze con chiave.

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

Per altre informazioni, vedere dotnet/runtime#64427.

Servizi del ciclo di vita ospitati

I servizi ospitati hanno ora più opzioni per l'esecuzione durante il ciclo di vita dell'applicazione. IHostedService forniva StartAsync e StopAsync e ora IHostedLifecycleService fornisce questi metodi aggiuntivi:

Questi metodi vengono eseguiti rispettivamente prima e dopo i punti esistenti.

L'esempio seguente illustra come usare le nuove 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;
    }
}

Per altre informazioni, vedere dotnet/runtime#86511.

Convalida delle opzioni

Generatore di origini

Per ridurre il sovraccarico di avvio e migliorare il set di funzionalità di convalida, è stato introdotto un generatore di codice sorgente che implementa la logica di convalida. Il codice seguente illustra modelli di esempio e classi di validator.

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 l'app usa l'inserimento delle dipendenze, è possibile inserire la convalida come illustrato nel codice di esempio seguente.

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 introduce il tipo ValidateOptionsResultBuilder per facilitare la creazione di un oggetto ValidateOptionsResult. È importante notare che questo generatore consente l'accumulo di più errori. In precedenza, la creazione dell'oggetto ValidateOptionsResult necessario per implementare IValidateOptions<TOptions>.Validate(String, TOptions) era difficile e talvolta generava errori di convalida a più livelli. In presenza di più errori, il processo di convalida spesso si arrestava al primo errore.

Il frammento di codice seguente mostra un esempio di utilizzo di 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();

Costruttori LoggerMessageAttribute

LoggerMessageAttribute offre ora overload di costruttori aggiuntivi. In precedenza era necessario scegliere il costruttore senza parametri o il costruttore che richiedeva tutti i parametri (ID evento, livello di log e messaggio). I nuovi overload offrono maggiore flessibilità per specificare i parametri necessari con codice ridotto. Se non si specifica un ID evento, il sistema ne genera uno automaticamente.

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

Metriche delle estensioni

Interfaccia IMeterFactory

È possibile registrare la nuova interfaccia IMeterFactory nei contenitori di inserimento delle dipendenze e usarla per creare oggetti Meter in modo isolato.

Registrare l'interfaccia IMeterFactory nel contenitore di inserimento delle dipendenze usando l'implementazione predefinita della factory del contatore:

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

I consumer possono quindi ottenere la factory del contatore e usarla per creare un nuovo oggetto Meter.

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

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

Meter meter = meterFactory.Create(options);

Classe MetricCollector<T>

La nuova classe MetricCollector<T> consente di registrare le misurazioni delle metriche insieme ai timestamp. Inoltre, la classe offre la flessibilità necessaria per usare un provider servizi orari di propria scelta per la generazione accurata dei timestamp.

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

Il pacchetto NuGet System.Numerics.Tensors aggiornato include API nel nuovo spazio dei nomi TensorPrimitives che aggiungono il supporto per le operazioni con i tensori. Le primitive dei tensori ottimizzano i carichi di lavoro con ingenti quantità di dati, come quelli di intelligenza artificiale e Machine Learning.

I carichi di lavoro di intelligenza artificiale, ad esempio la ricerca semantica e i processi RAG (Retrieval-Augmented Generation) estendono le funzionalità del linguaggio naturale dei modelli linguistici di grandi dimensioni (LLM, Large Language Model), ad esempio ChatGPT, potenziando i prompt con dati pertinenti. Per questi carichi di lavoro, sono fondamentali le operazioni sui vettori, ad esempio la similarità del coseno per trovare i dati più rilevanti per rispondere a una domanda. Il pacchetto System.Numerics.Tensors.TensorPrimitives fornisce API per le operazioni sui vettori, ovvero non è necessario accettare una dipendenza esterna o scrivere la propria implementazione.

Questo pacchetto sostituisce il pacchetto System.Numerics.Tensors.

Per altre informazioni, vedere il post di blog dell'annuncio di .NET 8 Preview 2.

Vedi anche