Programmazione asincrona

Usa metodi asincroni basati su task per evitare di bloccare un thread mentre Microsoft.Data.SqlClient attende l'I/O di connessione e rete. L'esecuzione asincrona non rende un comando o un lettore sicuro per l'uso concorrente, e non riduce i tempi di esecuzione delle query sul server.

Gli esempi che interrogano le tabelle applicative utilizzano il database di esempio AdventureWorks. Gli esempi di C# richiedono using Microsoft.Data.SqlClient;.

Converti un percorso di chiamata in codice asincrono

Converti un percorso di chiamata completo alla volta:

  1. Rendere asincrono il metodo che lo contiene.
  2. Sostituire le chiamate di connessione, comandi, lettori e transazioni con le controparti basate su attività.
  3. await dopo ogni operazione prima di utilizzare nuovamente lo stesso oggetto.
  4. Propaga il token di annullamento del chiamante a tutte le operazioni che il chiamante può annullare.
  5. Mantenere un timeout finito per i comandi come limite per le operazioni del database.

Usa await dall'apertura della connessione fino all'elaborazione dei risultati. Il seguente estratto assume che connectionString e sql siano già definiti. Crea un CancellationTokenSource con un timeout finito per l'operazione:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellation.Token);

using var command = new SqlCommand(sql, connection);
await using SqlDataReader reader =
    await command.ExecuteReaderAsync(cancellation.Token);

while (await reader.ReadAsync(cancellation.Token))
{
    Console.WriteLine(reader.GetValue(0));
}

ExecuteReaderAsync ritorna quando il primo risultato è disponibile, non dopo che tutte le righe sono arrivate. Ogni ReadAsync chiamata avanza di una riga, quindi il ciclo elabora le righe man mano che arrivano e rilascia il thread solo mentre aspetta altri dati dal server. Il token di 30 secondi limita l'intera operazione, incluso il loop di lettura.

Solo un'esecuzione asincrona può essere in sospeso su un SqlCommand. Solo una lettura asincrona, una transizione del risultato o un'operazione sul campo può essere in sospeso su un SqlDataReader. Attendi ogni operazione prima di iniziare la successiva sullo stesso oggetto.

Funzionalità della programmazione asincrona

I seguenti metodi sincroni hanno controparti basate su compiti che accettano un token di cancellazione:

Metodo sincrono Metodo asincrono Esempio
SqlConnection.Open SqlConnection.OpenAsync Convertire un metodo sincrono
SqlCommand.ExecuteNonQuery SqlCommand.ExecuteNonQueryAsync Esegui i comandi in modo asincrono
SqlCommand.ExecuteScalar SqlCommand.ExecuteScalarAsync Esegui i comandi in modo asincrono
SqlCommand.ExecuteXmlReader SqlCommand.ExecuteXmlReaderAsync Esegui i comandi in modo asincrono
SqlCommand.ExecuteReader SqlCommand.ExecuteReaderAsync Converti una catena di chiamate in codice asincrono
SqlDataReader.Read SqlDataReader.ReadAsync Convertire un percorso di chiamata in codice asincrono
SqlDataReader.NextResult SqlDataReader.NextResultAsync Leggi i risultati in modo asincrono
SqlDataReader.IsDBNull SqlDataReader.IsDBNullAsync Leggi i risultati in modo asincrono
SqlDataReader.GetFieldValue<T> SqlDataReader.GetFieldValueAsync<T> Leggi i risultati in modo asincrono
SqlBatch.ExecuteNonQuery, ExecuteScalar e ExecuteReader SqlBatch.ExecuteNonQueryAsync, ExecuteScalarAsync e ExecuteReaderAsync Esegui un batch
SqlBulkCopy.WriteToServer SqlBulkCopy.WriteToServerAsync Usa operazioni asincrone con SqlBulkCopy

Usa l'eliminazione sincrona quando un oggetto implementa solo IDisposable. Usa await using per oggetti che implementano IAsyncDisposable. Rilascia l'oggetto reader prima di riutilizzarne la connessione.

Esegui i comandi in modo asincrono

I seguenti esempi assumono che connection sia aperto. Usa ExecuteScalarAsync per un singolo valore e passa un token di cancellazione di proprietà del chiamante:

using var cancellation = new CancellationTokenSource(
    TimeSpan.FromSeconds(30));
CancellationToken token = cancellation.Token;

using var count = new SqlCommand(
    "SELECT COUNT(*) FROM Production.Product;",
    connection);
int total = (int)(await count.ExecuteScalarAsync(token))!;
Console.WriteLine($"Row count: {total}");

Usare ExecuteNonQueryAsync per le istruzioni che modificano i dati. Usa ExecuteXmlReaderAsync per una FOR XML query. Chiudi un lettore XML prima di eseguire un altro comando sulla stessa connessione. Senza Multiple Active Result Sets (MARS), una connessione permette solo un lettore aperto.

Leggi i risultati in modo asincrono

Il seguente esempio passa lo stesso token di cancellazione a ciascuna di queste chiamate asincrone:

  • ExecuteReaderAsync esegue la query.
  • ReadAsync legge ogni riga.
  • GetFieldValueAsync legge il valore di ogni colonna.
  • NextResultAsync passa al set di risultati successivo.
  • IsDBNullAsync verifica NULL.
using var cancellation = new CancellationTokenSource(
    TimeSpan.FromSeconds(30));
CancellationToken token = cancellation.Token;

using var query = new SqlCommand("""
    SELECT TOP (3) ProductID, Name
    FROM Production.Product
    ORDER BY ProductID;

    SELECT TOP (1) ProductID, Color
    FROM Production.Product
    WHERE Color IS NULL;
    """, connection);
await using SqlDataReader reader = await query.ExecuteReaderAsync(token);

while (await reader.ReadAsync(token))
{
    int id = await reader.GetFieldValueAsync<int>(0, token);
    string name = await reader.GetFieldValueAsync<string>(1, token);
    Console.WriteLine($"{id}: {name}");
}

if (await reader.NextResultAsync(token) && await reader.ReadAsync(token))
{
    int productId = await reader.GetFieldValueAsync<int>(0, token);
    bool colorIsNull = await reader.IsDBNullAsync(1, token);
    Console.WriteLine($"{productId}: Color is NULL: {colorIsNull}");
}

NextResultAsync passa al set di risultati successivo. GetFieldValueAsync e IsDBNullAsync leggono una colonna della riga corrente. Quando usi il valore predefinito CommandBehavior, la riga è già bufferizzata, quindi queste chiamate di solito si completano senza aspettare. Quando usi CommandBehavior.SequentialAccess, potrebbero dover attendere i dati delle colonne dalla rete. Per maggiori informazioni sull'accesso sequenziale, vedi supporto allo streaming SqlClient.

Converti un metodo sincrono

Questo metodo sincrono apre una connessione, esegue un comando e restituisce il numero di righe in sys.objects:

static int CountObjects(string connectionString)
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    using var command = new SqlCommand("SELECT COUNT(*) FROM sys.objects;", connection);
    return (int)command.ExecuteScalar()!;
}

Per convertire il metodo in codice asincrono, apporta queste modifiche:

  • Cambia il tipo di ritorno in Task<int>.
  • Aggiungi un CancellationToken parametro.
  • Sostituisci Open con OpenAsync, e sostituisci ExecuteScalar con ExecuteScalarAsync.
  • Chiudere la connessione con await using.

Il metodo convertito appare così:

static async Task<int> CountObjectsAsync(
    string connectionString,
    CancellationToken cancellationToken)
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync(cancellationToken);

    using var command = new SqlCommand("SELECT COUNT(*) FROM sys.objects;", connection);
    return (int)(await command.ExecuteScalarAsync(cancellationToken))!;
}

Il chiamante crea il token e attende il risultato:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
int objectCount = await CountObjectsAsync(connectionString, cancellation.Token);
Console.WriteLine($"Objects: {objectCount}");

Entrambi i metodi restituiscono lo stesso conteggio. L'output dipende dal database a cui ti connetti.

Connect Timeout Limita comunque l'instaurazione delle connessioni e le attese del pool. L'annullamento del token può restituire il controllo al chiamante prima.

Aggiungi chiamate asincrone a un'applicazione esistente

Converti un percorso di chiamata completo alla volta. Evita di chiamare .Result, .Wait(), o GetAwaiter().GetResult() su task del database perché queste chiamate bloccano e possono causare un deadlock in ambienti con un contesto di sincronizzazione.

Non avviare un comando asincrono e poi usare lo stesso comando o reader in modo sincrono prima che l'attività termini. Senza MARS, non usare la stessa connessione in modo sincrono prima che il compito sia terminato.

Aggiungi annullamento e limiti di tempo

Annullare un'operazione asincrona

Passa lo stesso token di cancellazione dall'apertura della connessione fino all'esecuzione del comando:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10));

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellation.Token);

using var command = new SqlCommand(
    "SELECT COUNT(*) FROM Production.Product;",
    connection);
await command.ExecuteScalarAsync(cancellation.Token);

La cancellazione avviene nei limiti del possibile. L'operazione potrebbe terminare prima che la cancellazione abbia effetto, e l'eccezione osservata dipende da dove avviene la cancellazione. Non dare per scontato che ricevere una richiesta di cancellazione significhi che il server ha annullato il lavoro già completato. Quando un insieme di modifiche deve avere successo o fallire come unità, si utilizza una transazione esplicita. Per maggiori informazioni, vedi Transazioni locali.

Cancel richiede l'annullamento del comando attuale. Non sostituisce un token di cancellazione di proprietà del chiamante. L'operazione potrebbe concludersi prima che la richiesta di cancellazione abbia effetto.

Dopo l'annullamento o il timeout, attendi l'operazione e rilascia il reader e il command. Controlla lo stato della connessione prima di riutilizzarla. Un guasto che interrompe la connessione richiede una nuova connessione.

Configurare il timeout

I timeout di connessione e i timeout dei comandi coprono diverse fasi:

Impostazione Coperture
Connect Timeout Creazione della connessione e attesa di una connessione del pool.
CommandTimeout Attese per l'esecuzione del comando e attese cumulative di lettura dalla rete.
Token di annullamento Una richiesta controllata dal chiamante di interrompere l'attesa.

CommandTimeout è espresso in secondi. 0 significa niente limite di tempo di comando. Il valore predefinito è il valore Command Timeout della connessione quando è configurata, altrimenti 30 secondi.

Per un lettore, il timeout del comando si applica all'attesa cumulativa di rete di ogni operazione di lettura del provider. Non include il tempo impiegato per elaborare una riga nel codice dell'applicazione. Un timeout può avvenire prima della prima riga o durante la recuperazione di una riga successiva.

Combina annullamento e timeout del comando

Usa insieme un timeout di comando finito e una cancellazione del chiamante. Questo esempio assume che connection sia aperto:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));

using var command = new SqlCommand("""
    SELECT TOP (10) SalesOrderID, OrderDate
    FROM Sales.SalesOrderHeader
    ORDER BY SalesOrderID;
    """, connection)
{
    CommandTimeout = 15
};

await using SqlDataReader reader =
    await command.ExecuteReaderAsync(cancellation.Token);

while (await reader.ReadAsync(cancellation.Token))
{
    Console.WriteLine($"{reader.GetInt32(0)} | {reader.GetDateTime(1):O}");
}

Se il chiamante cancella per primo, la chiamata su cui si attende genera un'eccezione. Il tipo di eccezione dipende da dove avviene l'annullamento. Se il timeout del comando scade per primo, il provider segnala un timeout. In entrambi i casi, il comando potrebbe completarsi con successo prima che la cancellazione o il timeout entri in vigore.

Modelli asincroni avanzati

Utilizzare metodi asincroni attraverso tipi di provider base

Il codice indipendente dal provider può utilizzare i metodi asincroni DbConnection, DbCommand e DbDataReader. Creare parametri tramite il comando provider ed evitare downcast, a meno che l'applicazione non abbia bisogno di una funzionalità specifica di Microsoft.Data.SqlClient.

Usa metodi di transazione SQL basati su attività

Usa i metodi di transazione locale basati sulle attività quando il framework target li espone. Il seguente esempio presuppone che connection sia aperto, che sql contenga il testo del comando e che il chiamante passi cancellationToken:

await using SqlTransaction transaction =
    (SqlTransaction)await connection.BeginTransactionAsync(cancellationToken);

using var command = new SqlCommand(sql, connection, transaction);

try
{
    await command.ExecuteNonQueryAsync(cancellationToken);
    await transaction.CommitAsync(cancellationToken);
}
catch (Exception operationException)
{
    try
    {
        await transaction.RollbackAsync(CancellationToken.None);
    }
    catch (Exception rollbackException)
    {
        throw new AggregateException(
            "The operation and transaction rollback both failed.",
            operationException,
            rollbackException);
    }

    throw;
}

Usa un token di pulizia separato per il rollback. Il token operativo potrebbe già essere stato cancellato. Gestire un fallimento di rollback senza scartare l'eccezione di operazione originale. Questo esempio preserva entrambe le eccezioni in un AggregateException. Un'applicazione può invece registrare l'eccezione di rollback e rilanciare l'eccezione originale.

Questi metodi Microsoft.Data.SqlClient utilizzano le implementazioni ereditate DbConnection e DbTransaction, che chiamano i metodi di transazione sincrona e restituiscono un'attività completata. Attenderli non rilascia il thread di chiamata mentre l'operazione di transazione è in corso. L'annullamento viene controllato prima dell'inizio dell'operazione e non interrompe un'operazione in corso. L'esecuzione dei comandi all'interno della transazione è ancora asincrona.

Usa transazioni distribuite asincrone

I comandi asincroni possono essere eseguiti all'interno di un TransactionScope. L'ambito definisce Transaction.Current, che .NET chiama la transazione ambient. Una connessione che si apre all'interno dell'ambito viene automaticamente inserita in quella transazione, perché la parola chiave di connessione Enlist è predefinita su true. Configura TransactionScopeAsyncFlowOption.Enabled quando la transazione deve propagarsi tra await. Le transazioni distribuite hanno requisiti di piattaforma, server e coordinatori. Per ulteriori informazioni, consulta integrazione System.Transactions.

Usa operazioni asincrone con SqlBulkCopy

Usa WriteToServerAsync per la copia asincrona in blocco. Mantieni attivo il lettore di origine o la raccolta di righe fino al completamento dell'attività restituita. Configura la dimensione del batch, il comportamento delle transazioni e il timeout per l'operazione. Per ulteriori informazioni, vedi Operazioni di copia in massa.

Usare in modo asincrono più comandi con MARS

MARS consente più di un comando in attesa su una connessione fisica. Abilitalo con MultipleActiveResultSets=True solo quando l'applicazione ha bisogno di quel modello di condivisione della connessione.

MARS non è necessario per la programmazione asincrona. Senza MARS, completare o chiudere il reader attivo prima di eseguire un altro comando sulla stessa connessione. Un secondo collegamento è spesso più adatto per lavori veramente indipendenti.

Per il comportamento delle sessioni e le restrizioni sulle transazioni, vedi Enable Multiple Active Result Sets.

Leggere e aggiornare in modo asincrono i dati con MARS

Quando abiliti MARS, un comando può leggere le righe mentre un altro comando viene eseguito sulla stessa connessione. Attendere ogni comando e operazione del lettore separatamente. Le operazioni MARS non sono thread-safe e l'esecuzione del server può comunque serializzarsi nei punti di yield.

Quando le operazioni di lettura e aggiornamento devono essere atomiche, assegnare la stessa transazione locale a ogni comando e testare il comportamento MARS della transazione. Non fare commit mentre un'altra operazione in quella transazione è ancora in sospeso.

Programmazione asincrona tradizionale

Microsoft.Data.SqlClient mantiene, per compatibilità, i metodi BeginExecuteNonQuery, BeginExecuteReader e BeginExecuteXmlReader del modello di programmazione asincrona (APM). Non usarli nel nuovo codice. CommandTimeout viene ignorato da queste chiamate asincrone in vecchio stile ed è rispettato da metodi basati su attività come ExecuteReaderAsync.

I metodi legacy non richiedono Asynchronous Processing=true. Microsoft. Data.SqlClient 4.0 e versioni successive non supportano la Asynchronous Processing proprietà di connessione.