Condividi tramite


Novità delle librerie .NET per .NET 10

Questo articolo descrive le nuove funzionalità nelle librerie .NET per .NET 10.

Crittografia

Trova i certificati mediante impronte digitali diverse da SHA-1

La ricerca di certificati in modo univoco tramite identificazione personale è un'operazione piuttosto comune, ma il X509Certificate2Collection.Find(X509FindType, Object, Boolean) metodo (per la FindByThumbprint modalità) cerca solo il valore di identificazione personale SHA-1.

C'è un rischio nell'usare il metodo Find per trovare le impronte SHA-2-256 ("SHA256") e SHA-3-256 poiché entrambi questi algoritmi di hash hanno le stesse lunghezze.

.NET 10 introduce invece un nuovo metodo che accetta il nome dell'algoritmo hash da usare per la corrispondenza.

X509Certificate2Collection coll = store.Certificates.FindByThumbprint(HashAlgorithmName.SHA256, thumbprint);
Debug.Assert(coll.Count < 2, "Collection has too many matches, has SHA-2 been broken?");
return coll.SingleOrDefault();

Trova dati con codifica PEM in ASCII/UTF-8

La codifica PEM (originariamente Privacy Enhanced Mail, ma ora usata ampiamente al di fuori del messaggio di posta elettronica) è definita per "text", il che significa che la classe PemEncoding è stata progettata per l'esecuzione su String e ReadOnlySpan<char>. Tuttavia, è comune ,in particolare in Linux, avere qualcosa di simile a un certificato scritto in un file che usa la codifica ASCII (stringa). Storicamente, ciò significava che era necessario aprire il file e convertire i byte in caratteri (o una stringa) prima di poter usare PemEncoding.

Il nuovo PemEncoding.FindUtf8(ReadOnlySpan<Byte>) metodo sfrutta il fatto che PEM è definito solo per i caratteri ASCII a 7 bit e che ASCII a 7 bit ha una sovrapposizione perfetta con valori UTF-8 a byte singolo. Chiamando questo nuovo metodo, è possibile ignorare la conversione da UTF-8/ASCII a char e leggere direttamente il file.

byte[] fileContents = File.ReadAllBytes(path);
-char[] text = Encoding.ASCII.GetString(fileContents);
-PemFields pemFields = PemEncoding.Find(text);
+PemFields pemFields = PemEncoding.FindUtf8(fileContents);

-byte[] contents = Base64.DecodeFromChars(text.AsSpan()[pemFields.Base64Data]);
+byte[] contents = Base64.DecodeFromUtf8(fileContents.AsSpan()[pemFields.Base64Data]);

Algoritmo di crittografia per l'esportazione PKCS#12/PFX

I nuovi ExportPkcs12 metodi su X509Certificate consentono ai chiamanti di scegliere quali algoritmi di crittografia e digest vengono usati per produrre l'output.

  • Pkcs12ExportPbeParameters.Pkcs12TripleDesSha1 indica lo standard di fatto dell'era windows XP. Produce un output supportato da quasi ogni libreria e piattaforma che supporta la lettura di PKCS#12/PFX scegliendo un algoritmo di crittografia meno recente.
  • Pkcs12ExportPbeParameters.Pbes2Aes256Sha256 indica che AES deve essere usato invece di 3DES (e SHA-2-256 anziché SHA-1), ma l'output potrebbe non essere compreso da tutti i lettori (ad esempio Windows XP).

Se si desidera un maggiore controllo, è possibile usare l'overload che accetta un oggetto PbeParameters.

Crittografia post-quantistica (PQC)

.NET 10 include il supporto per tre nuovi algoritmi asimmetrici: ML-KEM (FIPS 203), ML-DSA (FIPS 204) e SLH-DSA (FIPS 205). I nuovi tipi sono:

Poiché aggiunge poco vantaggio, questi nuovi tipi non derivano da AsymmetricAlgorithm. Anziché l'approccio AsymmetricAlgorithm alla creazione di un oggetto e quindi all'importazione di una chiave in esso o alla generazione di una chiave aggiornata, i nuovi tipi usano tutti i metodi statici per generare o importare una chiave:

using System;
using System.IO;
using System.Security.Cryptography;

private static bool ValidateMLDsaSignature(ReadOnlySpan<byte> data, ReadOnlySpan<byte> signature, string publicKeyPath)
{
    string publicKeyPem = File.ReadAllText(publicKeyPath);

    using (MLDsa key = MLDsa.ImportFromPem(publicKeyPem))
    {
        return key.VerifyData(data, signature);
    }
}

E invece di impostare le proprietà degli oggetti e avere una chiave materializzarsi, la generazione di chiavi in questi nuovi tipi accetta tutte le opzioni necessarie.

using (MLKem key = MLKem.GenerateKey(MLKemAlgorithm.MLKem768))
{
    string publicKeyPem = key.ExportSubjectPublicKeyInfoPem();
    ...
}

Tutti questi algoritmi continuano con il modello di avere una proprietà statica IsSupported per indicare se l'algoritmo è supportato nel sistema corrente.

.NET 10 include l'API di crittografia Windows: supporto CNG (Next Generation) per la crittografia post-quantistica (PQC), che rende questi algoritmi disponibili nei sistemi Windows con supporto PQC. Per esempio:

using System;
using System.IO;
using System.Security.Cryptography;

private static bool ValidateMLDsaSignature(ReadOnlySpan<byte> data, ReadOnlySpan<byte> signature, string publicKeyPath)
{
    string publicKeyPem = File.ReadAllText(publicKeyPath);

    using MLDsa key = MLDsa.ImportFromPem(publicKeyPem);
    return key.VerifyData(data, signature);
}

Gli algoritmi PQC sono disponibili nei sistemi in cui le librerie di crittografia di sistema sono OpenSSL 3.5 (o versioni successive) o Windows CNG con supporto PQC. Il MLKem tipo non è contrassegnato come [Experimental], ma alcuni dei relativi metodi sono (e saranno fino a quando non vengono finalizzati gli standard sottostanti). Le classi MLDsa, SlhDsa e CompositeMLDsa vengono contrassegnate come [Experimental] sotto diagnostica SYSLIB5006 fino al completamento dello sviluppo.

ML-DSA

La MLDsa classe include funzionalità di facilità d'uso che semplificano i modelli di codice comuni:

private static byte[] SignData(string privateKeyPath, ReadOnlySpan<byte> data)
{
    using (MLDsa signingKey = MLDsa.ImportFromPem(File.ReadAllBytes(privateKeyPath)))
    {
-       byte[] signature = new byte[signingKey.Algorithm.SignatureSizeInBytes];
-       signingKey.SignData(data, signature);
+       return signingKey.SignData(data);
-       return signature;
    }
}

.NET 10 aggiunge inoltre il supporto per HashML-DSA, denominato "PreHash" per distinguerlo da ML-DSA "puro". Quando la specifica sottostante interagisce con il valore OID (Object Identifier), i metodi SignPreHash e VerifyPreHash su questo [Experimental] tipo accettano l'OID decimale punteggiato come stringa. Questo può evolversi man mano che più scenari che usano HashML-DSA diventano ben definiti.

private static byte[] SignPreHashSha3_256(MLDsa signingKey, ReadOnlySpan<byte> data)
{
    const string Sha3_256Oid = "2.16.840.1.101.3.4.2.8";
    return signingKey.SignPreHash(SHA3_256.HashData(data), Sha3_256Oid);
}

A partire da RC 1, ML-DSA supporta anche le firme create e verificate da un valore mu "esterno", che offre maggiore flessibilità per scenari di crittografia avanzati:

private static byte[] SignWithExternalMu(MLDsa signingKey, ReadOnlySpan<byte> externalMu)
{
    return signingKey.SignMu(externalMu);
}

private static bool VerifyWithExternalMu(MLDsa verifyingKey, ReadOnlySpan<byte> externalMu, ReadOnlySpan<byte> signature)
{
    return verifyingKey.VerifyMu(externalMu, signature);
}

ML-DSA compositi

.NET 10 introduce nuovi tipi per supportare ietf-lamps-pq-composite-sigs (al draft 8 di .NET 10 GA), inclusi i CompositeMLDsa e CompositeMLDsaAlgorithm, con l'implementazione dei metodi primitivi per le varianti RSA.

var algorithm = CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss;
using var privateKey = CompositeMLDsa.GenerateKey(algorithm);

byte[] data = [42];
byte[] signature = privateKey.SignData(data);

using var publicKey = CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, privateKey.ExportCompositeMLDsaPublicKey());
Console.WriteLine(publicKey.VerifyData(data, signature)); // True

signature[0] ^= 1; // Tamper with signature
Console.WriteLine(publicKey.VerifyData(data, signature)); // False

AES KeyWrap with Padding (IETF RFC 5649)

AES-KWP è un algoritmo usato occasionalmente in costruzioni come Cryptographic Message Syntax (CMS) EnvelopedData, dove il contenuto viene crittografato una sola volta, ma la chiave di decrittografia deve essere distribuita a più parti, ognuna in un formato segreto distinto.

.NET supporta ora l'algoritmo AES-KWP tramite metodi di istanza nella Aes classe :

private static byte[] DecryptContent(ReadOnlySpan<byte> kek, ReadOnlySpan<byte> encryptedKey, ReadOnlySpan<byte> ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.SetKey(kek);

        Span<byte> dek = stackalloc byte[256 / 8];
        int length = aes.DecryptKeyWrapPadded(encryptedKey, dek);

        aes.SetKey(dek.Slice(0, length));
        return aes.DecryptCbc(ciphertext);
    }
}

Globalizzazione e data/ora

Nuovi overload del metodo in ISOWeek per il tipo DateOnly

La classe ISOWeek è stata originariamente progettata per funzionare esclusivamente con DateTime, come è stato introdotto prima dell'esistenza del tipo DateOnly. Ora che DateOnly è disponibile, ha senso anche ISOWeek supportarlo. I seguenti sovraccarichi sono nuovi:

Ordinamento numerico per il confronto tra stringhe

Il confronto di stringhe numeriche è una funzionalità molto richiesta per confrontare le stringhe numericamente anziché lessicograficamente. Ad esempio, 2 è minore di 10, quindi "2" dovrebbe essere visualizzato prima di "10" quando ordinato numericamente. Analogamente, "2" e "02" sono uguali numericamente. Con la nuova NumericOrdering opzione, è ora possibile eseguire questi tipi di confronti:

StringComparer numericStringComparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);

Console.WriteLine(numericStringComparer.Equals("02", "2"));
// Output: True

foreach (string os in new[] { "Windows 8", "Windows 10", "Windows 11" }.Order(numericStringComparer))
{
    Console.WriteLine(os);
}

// Output:
// Windows 8
// Windows 10
// Windows 11

HashSet<string> set = new HashSet<string>(numericStringComparer) { "007" };
Console.WriteLine(set.Contains("7"));
// Output: True

Questa opzione non è valida per le operazioni stringa basate su indice seguenti: IndexOf, LastIndexOf, StartsWith, EndsWith, IsPrefixe IsSuffix.

Nuovo sovraccarico TimeSpan.FromMilliseconds con un singolo parametro

Il metodo TimeSpan.FromMilliseconds(Int64, Int64) è stato introdotto in precedenza senza aggiungere un overload che accetta un singolo parametro.

Anche se funziona perché il secondo parametro è facoltativo, genera un errore di compilazione quando viene usato in un'espressione LINQ, ad esempio:

Expression<Action> a = () => TimeSpan.FromMilliseconds(1000);

Il problema si verifica perché le espressioni LINQ non possono gestire parametri facoltativi. Per risolvere questo problema, .NET 10 introduce un nuovo overload che accetta un singolo parametro. Modifica anche il metodo esistente per rendere obbligatorio il secondo parametro.

Stringhe

API di normalizzazione delle stringhe da usare con l'intervallo di caratteri

La normalizzazione delle stringhe Unicode è stata supportata da molto tempo, ma le API esistenti funzionano solo con il tipo stringa. Ciò significa che i chiamanti con dati archiviati in forme diverse, ad esempio matrici di caratteri o intervalli, devono allocare una nuova stringa per usare queste API. Inoltre, le API che restituiscono una stringa normalizzata allocano sempre una nuova stringa per rappresentare l'output normalizzato.

.NET 10 introduce nuove API che funzionano con intervalli di caratteri, che espandono la normalizzazione oltre i tipi di stringa e consentono di evitare allocazioni non necessarie:

Supporto UTF-8 per la conversione di stringa esadecimale

.NET 10 aggiunge il supporto UTF-8 per le operazioni di conversione di stringhe esadecimale nella Convert classe . Questi nuovi metodi offrono modi efficienti per eseguire la conversione tra sequenze di byte UTF-8 e rappresentazioni esadecimali senza richiedere allocazioni di stringhe intermedie:

Questi metodi rispecchiano gli overload esistenti che funzionano con string e ReadOnlySpan<char>, ma operano direttamente in byte codificati UTF-8 per migliorare le prestazioni negli scenari in cui si lavora già con i dati UTF-8.

Collezioni

Sovraccarichi aggiuntivi per TryAdd e TryGetValue in OrderedDictionary<TKey, TValue>

OrderedDictionary<TKey,TValue> fornisce TryAdd e TryGetValue per l'aggiunta e il recupero come qualsiasi altra implementazione di IDictionary<TKey, TValue>. Esistono tuttavia scenari in cui è possibile eseguire altre operazioni, quindi vengono aggiunti nuovi overload che restituiscono un indice alla voce:

Questo indice può quindi essere usato con GetAt e SetAt per un accesso rapido all'entry. Un esempio di utilizzo del nuovo TryAdd overload consiste nell'aggiungere o aggiornare una coppia chiave-valore nel dizionario ordinato:

// Try to add a new key with value 1.
if (!orderedDictionary.TryAdd(key, 1, out int index))
{
    // Key was present, so increment the existing value instead.
    int value = orderedDictionary.GetAt(index).Value;
    orderedDictionary.SetAt(index, value + 1);
}

Questa nuova API è già utilizzata in JsonObject e migliora le prestazioni degli aggiornamenti delle proprietà di 10-20%.

Serializzazione

Consenti di specificare ReferenceHandler in JsonSourceGenerationOptions

Quando si usano generatori di origine per la serializzazione JSON, il contesto generato genera un'eccezione quando i cicli vengono serializzati o deserializzati. Ora puoi personalizzare questo comportamento specificando il ReferenceHandler nel JsonSourceGenerationOptionsAttribute. Ecco un esempio che usa JsonKnownReferenceHandler.Preserve:

public static void MakeSelfRef()
{
    SelfReference selfRef = new SelfReference();
    selfRef.Me = selfRef;

    Console.WriteLine(JsonSerializer.Serialize(selfRef, ContextWithPreserveReference.Default.SelfReference));
    // Output: {"$id":"1","Me":{"$ref":"1"}}
}

[JsonSourceGenerationOptions(ReferenceHandler = JsonKnownReferenceHandler.Preserve)]
[JsonSerializable(typeof(SelfReference))]
internal partial class ContextWithPreserveReference : JsonSerializerContext
{
}

internal class SelfReference
{
    public SelfReference Me { get; set; } = null!;
}

Opzione per impedire le proprietà JSON duplicate

La specifica JSON non specifica come gestire le proprietà duplicate durante la deserializzazione di un payload JSON. Ciò può causare risultati imprevisti e vulnerabilità di sicurezza. .NET 10 introduce l'opzione JsonSerializerOptions.AllowDuplicateProperties per impedire le proprietà JSON duplicate:

string json = """{ "Value": 1, "Value": -1 }""";
Console.WriteLine(JsonSerializer.Deserialize<MyRecord>(json).Value); // -1

JsonSerializerOptions options = new() { AllowDuplicateProperties = false };
JsonSerializer.Deserialize<MyRecord>(json, options);                // throws JsonException
JsonSerializer.Deserialize<JsonObject>(json, options);              // throws JsonException
JsonSerializer.Deserialize<Dictionary<string, int>>(json, options); // throws JsonException

JsonDocumentOptions docOptions = new() { AllowDuplicateProperties = false };
JsonDocument.Parse(json, docOptions);   // throws JsonException

record MyRecord(int Value);

I duplicati vengono rilevati controllando se un valore viene assegnato più volte durante la deserializzazione, quindi funziona come previsto con altre opzioni come la distinzione tra maiuscole e minuscole e i criteri di denominazione.

Opzioni di serializzazione JSON rigorose

Il serializzatore JSON accetta molte opzioni per personalizzare la serializzazione e la deserializzazione, ma le impostazioni predefinite potrebbero essere troppo rilassate per alcune applicazioni. .NET 10 aggiunge un nuovo JsonSerializerOptions.Strict set di impostazioni che segue le procedure consigliate includendo le opzioni seguenti:

Queste opzioni sono compatibili con la lettura: JsonSerializerOptions.Default un oggetto serializzato con JsonSerializerOptions.Default può essere deserializzato con JsonSerializerOptions.Strict.

Per altre informazioni sulla serializzazione JSON, vedere Panoramica di System.Text.Json.

Supporto di PipeReader per serializzatore JSON

JsonSerializer.Deserialize supporta PipeReaderora , completando il supporto esistente PipeWriter . In precedenza, la deserializzazione da una PipeReader richiesta di conversione in un Streamoggetto , ma i nuovi overload eliminano tale passaggio integrandolo PipeReader direttamente nel serializzatore. Come bonus, non dover convertire da quello che si sta già tenendo può produrre alcuni vantaggi di efficienza.

In questo modo viene illustrato l'utilizzo di base:

using System;
using System.IO.Pipelines;
using System.Text.Json;
using System.Threading.Tasks;

var pipe = new Pipe();

// Serialize to writer
await JsonSerializer.SerializeAsync(pipe.Writer, new Person("Alice"));
await pipe.Writer.CompleteAsync();

// Deserialize from reader
var result = await JsonSerializer.DeserializeAsync<Person>(pipe.Reader);
await pipe.Reader.CompleteAsync();

Console.WriteLine($"Your name is {result.Name}.");
// Output: Your name is Alice.

record Person(string Name);

Di seguito è riportato un esempio di producer che produce token in blocchi e un consumer che riceve e li visualizza:

using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Text.Json;
using System.Threading.Tasks;

var pipe = new Pipe();

// Producer writes to the pipe in chunks.
var producerTask = Task.Run(async () =>
{
    async static IAsyncEnumerable<Chunk> GenerateResponse()
    {
        yield return new Chunk("The quick brown fox", DateTime.Now);
        await Task.Delay(500);
        yield return new Chunk(" jumps over", DateTime.Now);
        await Task.Delay(500);
        yield return new Chunk(" the lazy dog.", DateTime.Now);
    }

    await JsonSerializer.SerializeAsync<IAsyncEnumerable<Chunk>>(pipe.Writer, GenerateResponse());
    await pipe.Writer.CompleteAsync();
});

// Consumer reads from the pipe and outputs to console.
var consumerTask = Task.Run(async () =>
{
    var thinkingString = "...";
    var clearThinkingString = new string("\b\b\b");
    var lastTimestamp = DateTime.MinValue;

    // Read response to end.
    Console.Write(thinkingString);
    await foreach (var chunk in JsonSerializer.DeserializeAsyncEnumerable<Chunk>(pipe.Reader))
    {
        Console.Write(clearThinkingString);
        Console.Write(chunk.Message);
        Console.Write(thinkingString);
        lastTimestamp = DateTime.Now;
    }

    Console.Write(clearThinkingString);
    Console.WriteLine($" Last message sent at {lastTimestamp}.");

    await pipe.Reader.CompleteAsync();
});

await producerTask;
await consumerTask;

record Chunk(string Message, DateTime Timestamp);

Tutto questo viene serializzato come JSON in Pipe (formattato qui per la leggibilità):

[
    {
        "Message": "The quick brown fox",
        "Timestamp": "2025-08-01T18:37:27.2930151-07:00"
    },
    {
        "Message": " jumps over",
        "Timestamp": "2025-08-01T18:37:27.8594502-07:00"
    },
    {
        "Message": " the lazy dog.",
        "Timestamp": "2025-08-01T18:37:28.3753669-07:00"
    }
]

Sistema.Numerici

Altri metodi di trasformazione di matrice con mano sinistra

.NET 10 aggiunge le API rimanenti per la creazione di matrici di trasformazione a sinistra per le matrici di cartelloni pubblicitari e con vincoli. È possibile usare questi metodi come le versioni esistenti per destri, ad esempio CreateBillboard(Vector3, Vector3, Vector3, Vector3), quando si utilizza invece un sistema di coordinate mancino:

Miglioramenti del tensore

Lo System.Numerics.Tensors spazio dei nomi include ora un'interfaccia non generica, IReadOnlyTensor, per operazioni come l'accesso a Lengths e Strides. Le operazioni di sezione non copiano più i dati, migliorando le prestazioni. Inoltre, è possibile accedere ai dati in modo non generico tramite boxing a object quando le prestazioni non sono critiche.

Le API tensor sono ora stabili e non sono più contrassegnate come sperimentali. Anche se le API richiedono ancora di fare riferimento al pacchetto NuGet System.Numerics.Tensors , sono state esaminate e completate per la versione di .NET 10. I tipi sfruttano gli operatori di estensione C# 14 per fornire operazioni aritmetiche quando il tipo T sottostante supporta l'operazione. Se T implementa le interfacce matematiche generiche pertinenti, IAdditionOperators<TSelf, TOther, TResult> ad esempio o INumber<TSelf>, l'operazione è supportata. Ad esempio, tensor + tensor è disponibile per un Tensor<int>oggetto , ma non è disponibile per un oggetto Tensor<bool>.

Convalida delle opzioni

Nuovo costruttore sicuro AOT per ValidationContext

La ValidationContext classe , usata durante la convalida delle opzioni, include un nuovo overload del costruttore che accetta in modo esplicito il displayName parametro :

ValidationContext(Object, String, IServiceProvider, IDictionary<Object,Object>)

Il nome visualizzato garantisce la sicurezza AOT e ne abilita l'uso nelle compilazioni native senza avvisi.

Diagnostica

Supporto per gli URL dello schema di telemetria in ActivitySource e Meter

ActivitySource e Meter ora supportano la specifica di un URL dello schema di telemetria durante la costruzione, allineato alle specifiche OpenTelemetry. Lo schema di telemetria garantisce coerenza e compatibilità per i dati di traccia e metriche. .NET 10 introduce ActivitySourceOptionsinoltre , che semplifica la creazione di istanze con più opzioni di configurazione (incluso ActivitySource di telemetria).

Le nuove API sono:

La Activity classe abilita la traccia distribuita monitorando il flusso delle operazioni tra servizi o componenti. .NET supporta la serializzazione di questi dati di traccia fuori dal processo tramite il provider sorgente di eventi Microsoft-Diagnostics-DiagnosticSource. Un Activity oggetto può includere metadati aggiuntivi, ad ActivityLink esempio e ActivityEvent. .NET 10 aggiunge il supporto per la serializzazione di questi collegamenti ed eventi, quindi i dati di traccia fuori dal processo ora includono tali informazioni. Per esempio:

Events->"[(TestEvent1,​2025-03-27T23:34:10.6225721+00:00,​[E11:​EV1,​E12:​EV2]),​(TestEvent2,​2025-03-27T23:34:11.6276895+00:00,​[E21:​EV21,​E22:​EV22])]"
Links->"[(19b6e8ea216cb2ba36dd5d957e126d9f,​98f7abcb3418f217,​Recorded,​null,​false,​[alk1:​alv1,​alk2:​alv2]),​(2d409549aadfdbdf5d1892584a5f2ab2,​4f3526086a350f50,​None,​null,​false)]"

Supporto per la limitazione del campionamento delle tracce in base alla frequenza

Quando i dati di traccia distribuiti vengono serializzati al di fuori del processo tramite il provider dell'origine degli eventi Microsoft-Diagnostics-DiagnosticSource, è possibile emettere tutte le attività registrate o applicare il campionamento in base a un rapporto di traccia.

Una nuova opzione di campionamento denominata Campionamento di limitazione della frequenza limita il numero di attività radice serializzate al secondo. Ciò consente di controllare più precisamente il volume dei dati.

Gli aggregatori di dati di traccia fuori dal processo possono abilitare e configurare questo campionamento specificando l'opzione in FilterAndPayloadSpecs. Ad esempio, l'impostazione seguente limita la serializzazione a 100 attività radice al secondo in tutte le ActivitySource istanze:

[AS]*/-ParentRateLimitingSampler(100)

File ZIP

Miglioramenti delle prestazioni e della memoria zipArchive

.NET 10 migliora le prestazioni e l'utilizzo della memoria di ZipArchive.

In primo luogo, il processo di registrazione delle voci su un ZipArchive è stato ottimizzato quando si è in modalità Update. In precedenza, tutte le istanze di ZipArchiveEntry venivano caricate in memoria e riscritte, il che poteva causare un elevato utilizzo della memoria e colli di bottiglia nelle prestazioni. L'ottimizzazione riduce l'utilizzo della memoria e migliora le prestazioni evitando la necessità di caricare tutte le voci in memoria.

In secondo luogo, l'estrazione delle voci di ZipArchive è ora parallelizzata e le strutture di dati interne sono ottimizzate per un uso più efficiente della memoria. Questi miglioramenti consentono di risolvere i problemi relativi ai colli di bottiglia delle prestazioni e all'utilizzo elevato della memoria, rendendo ZipArchive più efficiente e veloce, soprattutto quando si gestiscono archivi di grandi dimensioni.

Nuove API ZIP asincrone

.NET 10 introduce nuove API asincrone che semplificano l'esecuzione di operazioni non bloccanti durante la lettura o la scrittura in file ZIP. Questa funzionalità è stata fortemente richiesta dalla community.

Sono disponibili nuovi async metodi per estrarre, creare e aggiornare gli archivi ZIP. Questi metodi consentono agli sviluppatori di gestire in modo efficiente file di grandi dimensioni e migliorare la velocità di risposta dell'applicazione, in particolare negli scenari che coinvolgono operazioni associate a I/O. Questi metodi includono:

Per esempi di uso di queste API, vedere il post di blog preview 4.

Miglioramento delle prestazioni in GZipStream per i flussi concatenati

Un contributo della community ha migliorato le prestazioni di GZipStream quando si elaborano flussi di dati GZip concatenati. In precedenza, ogni nuovo segmento di flusso eliminava e riallocava l'interno ZLibStreamHandle, il che comportava allocazioni di memoria aggiuntive e costi di inizializzazione aggiuntivi. Con questa modifica, l'handle viene ora reimpostato e riutilizzato per ridurre le allocazioni di memoria gestite e non gestite e migliorare il tempo di esecuzione. L'impatto maggiore (circa 35% più veloce) si verifica durante l'elaborazione di un numero elevato di flussi di dati di piccole dimensioni. Questa modifica:

  • Elimina l'allocazione ripetuta di ~64-80 byte di memoria per ogni flusso concatenato, con risparmi aggiuntivi sulla memoria non gestita.
  • Riduce il tempo di esecuzione di circa 400 ns per flusso concatenato.

Gestione dei processi di Windows

Avviare processi Windows in un nuovo gruppo di processi

Per Windows, è ora possibile usare ProcessStartInfo.CreateNewProcessGroup per avviare un processo in un gruppo di processi separato. In questo modo è possibile inviare segnali isolati ai processi figlio che altrimenti potrebbero arrestare l'elemento padre senza una corretta gestione. L'invio di segnali è utile per evitare la terminazione forzata.

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        bool isChildProcess = args.Length > 0 && args[0] == "child";
        if (!isChildProcess)
        {
            var psi = new ProcessStartInfo
            {
                FileName = Environment.ProcessPath,
                Arguments = "child",
                CreateNewProcessGroup = true,
            };

            using Process process = Process.Start(psi)!;
            Thread.Sleep(5_000);

            GenerateConsoleCtrlEvent(CTRL_C_EVENT, (uint)process.Id);
            process.WaitForExit();

            Console.WriteLine("Child process terminated gracefully, continue with the parent process logic if needed.");
        }
        else
        {
            // If you need to send a CTRL+C, the child process needs to re-enable CTRL+C handling, if you own the code, you can call SetConsoleCtrlHandler(NULL, FALSE).
            // see https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw#remarks
            SetConsoleCtrlHandler((IntPtr)null, false);

            Console.WriteLine("Greetings from the child process!  I need to be gracefully terminated, send me a signal!");

            bool stop = false;

            var registration = PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx =>
            {
                stop = true;
                ctx.Cancel = true;
                Console.WriteLine("Received CTRL+C, stopping...");
            });

            StreamWriter sw = File.AppendText("log.txt");
            int i = 0;
            while (!stop)
            {
                Thread.Sleep(1000);
                sw.WriteLine($"{++i}");
                Console.WriteLine($"Logging {i}...");
            }

            // Clean up
            sw.Dispose();
            registration.Dispose();

            Console.WriteLine("Thanks for not killing me!");
        }
    }

    private const int CTRL_C_EVENT = 0;
    private const int CTRL_BREAK_EVENT = 1;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetConsoleCtrlHandler(IntPtr handler, [MarshalAs(UnmanagedType.Bool)] bool Add);

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
}

Miglioramenti di WebSocket

WebSocketStream

.NET 10 introduce WebSocketStream, una nuova API progettata per semplificare alcuni degli scenari più comuni, e in precedenza complessi,WebSocket in .NET.

Le API tradizionali WebSocket sono di basso livello e richiedono un boilerplate significativo: gestione del buffering e frame, ricostruzione dei messaggi, gestione della codifica/decodifica e scrittura di wrapper personalizzati per l'integrazione con flussi, canali o altre astrazioni di trasporto. Queste complessità rendono difficile usare WebSocket come trasporto, soprattutto per le app con protocolli basati su streaming o testo o gestori basati su eventi.

WebSocketStream risolve questi punti di dolore fornendo un'astrazione Streambasata su un WebSocket. Ciò consente un'integrazione senza problemi con le API esistenti per la lettura, la scrittura e l'analisi dei dati, sia binari che di testo, e riduce la necessità di tubazioni manuali.

WebSocketStream abilita API di alto livello e familiari per modelli di consumo e produzione WebSocket comuni. Queste API riducono l'attrito e semplificano l'implementazione di scenari avanzati.

Modelli di utilizzo comuni

Ecco alcuni esempi di come WebSocketStream semplifica i flussi di lavoro tipici WebSocket :

Protocollo di testo di streaming (ad esempio, STOMP)
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

// Streaming text protocol (for example, STOMP).
using Stream transportStream = WebSocketStream.Create(
    connectedWebSocket, 
    WebSocketMessageType.Text,
    ownsWebSocket: true);
// Integration with Stream-based APIs.
// Don't close the stream, as it's also used for writing.
using var transportReader = new StreamReader(transportStream, leaveOpen: true); 
var line = await transportReader.ReadLineAsync(cancellationToken); // Automatic UTF-8 and new line handling.
transportStream.Dispose(); // Automatic closing handshake handling on `Dispose`.
Protocollo binario di streaming (ad esempio, AMQP)
using System;
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

// Streaming binary protocol (for example, AMQP).
Stream transportStream = WebSocketStream.Create(
    connectedWebSocket,
    WebSocketMessageType.Binary,
    closeTimeout: TimeSpan.FromSeconds(10));
await message.SerializeToStreamAsync(transportStream, cancellationToken);
var receivePayload = new byte[payloadLength];
await transportStream.ReadExactlyAsync(receivePayload, cancellationToken);
transportStream.Dispose();
// `Dispose` automatically handles closing handshake.
Leggere un singolo messaggio come flusso (ad esempio, deserializzazione JSON)
using System.IO;
using System.Net.WebSockets;
using System.Text.Json;

// Reading a single message as a stream (for example, JSON deserialization).
using Stream messageStream = WebSocketStream.CreateReadableMessageStream(connectedWebSocket, WebSocketMessageType.Text);
// JsonSerializer.DeserializeAsync reads until the end of stream.
var appMessage = await JsonSerializer.DeserializeAsync<AppMessage>(messageStream);
Scrivere un singolo messaggio come flusso (ad esempio, serializzazione binaria)
using System;
using System.IO;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

// Writing a single message as a stream (for example, binary serialization).
public async Task SendMessageAsync(AppMessage message, CancellationToken cancellationToken)
{
    using Stream messageStream = WebSocketStream.CreateWritableMessageStream(_connectedWebSocket, WebSocketMessageType.Binary);
    foreach (ReadOnlyMemory<byte> chunk in message.SplitToChunks())
    {
        await messageStream.WriteAsync(chunk, cancellationToken);
    }
} // EOM sent on messageStream.Dispose().

Miglioramenti di TLS

TLS 1.3 per macOS (client)

.NET 10 aggiunge il supporto TLS 1.3 sul lato client in macOS integrando Network.framework di Apple in SslStream e HttpClient. In passato macOS usava il trasporto sicuro che non supporta TLS 1.3; il consenso esplicito in Network.framework abilita TLS 1.3.

Ambito e comportamento

  • solo macOS, lato client in questa versione.
  • Acconsentire esplicitamente. Le app esistenti continuano a usare lo stack corrente, a meno che non sia abilitato.
  • Se abilitata, le versioni precedenti di TLS (TLS 1.0 e 1.1) potrebbero non essere più disponibili tramite Network.framework.

Come abilitare

Usare un'opzione AppContext nel codice:

// Opt in to Network.framework-backed TLS on Apple platforms.
AppContext.SetSwitch("System.Net.Security.UseNetworkFramework", true);

using var client = new HttpClient();
var html = await client.GetStringAsync("https://example.com");

In alternativa, usare una variabile di ambiente:

# Opt-in via environment variable (set for the process or machine as appropriate)
DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=1
# or
DOTNET_SYSTEM_NET_SECURITY_USENETWORKFRAMEWORK=true

Note

  • TLS 1.3 si applica alle SslStream API e basate su di essa (ad esempio,HttpClient/HttpMessageHandler ).
  • I pacchetti di crittografia sono controllati da macOS tramite Network.framework.
  • Il comportamento del flusso sottostante può essere diverso quando Network.framework è abilitato (ad esempio, buffering, completamento di lettura/scrittura, semantica di annullamento).
  • La semantica può differire per le letture a zero byte. Evitare di basarsi sulle letture di lunghezza zero per rilevare la disponibilità dei dati.
  • Alcuni nomi host IDN (Internationalized Domain Names) potrebbero essere rifiutati da Network.framework. Preferisce nomi host ASCII/Punycode (A-label) o convalidare i nomi rispetto ai vincoli macOS/Network.framework.
  • Se l'app si basa su un comportamento specifico SslStream del caso perimetrale, convalidarlo in Network.framework.