Programación asincrónica

Utiliza métodos asíncronos basados en tareas para evitar bloquear un hilo mientras Microsoft. Data.SqlClient espera la conexión y la E/S de red. La ejecución asíncrona no hace que un comando o lector sea seguro para uso simultáneo, ni reduce el tiempo de ejecución de consultas en el servidor.

Los ejemplos que consultan tablas de aplicación utilizan la base de datos de ejemplo AdventureWorks. Los ejemplos de C# requieren using Microsoft.Data.SqlClient;.

Convertir una secuencia de llamadas en código asíncrono

Convierte una ruta de llamada completa a la vez:

  1. Haz que el método contenedor sea asincrónico.
  2. Sustituya llamadas de conexión, comando, lector y transacción por sus equivalentes basados en tareas.
  3. await complete cada operación antes de volver a usar el mismo objeto.
  4. Pasa el token de cancelación del llamante en cada operación que el llamante pueda cancelar.
  5. Mantén un tiempo de espera de comando finito como límite de operación de la base de datos.

Use await desde la apertura de la conexión hasta el procesamiento de resultados. El siguiente extracto asume que connectionString y sql ya están definidos. Crea un CancellationTokenSource con un tiempo límite finito para la operación:

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 devuelve cuando el primer resultado está disponible, no después de que lleguen todas las filas. Cada llamada a ReadAsync avanza una fila, así que el bucle procesa las filas a medida que llegan y libera el hilo solo mientras espera más datos del servidor. El token de 30 segundos limita toda la operación, incluido el bucle de lectura.

Solo puede estar pendiente una ejecución asíncrona en un SqlCommand. Solo puede haber pendiente una lectura asíncrona, una transición de resultado o una operación de campo para un SqlDataReader. Espera cada operación antes de comenzar la siguiente operación en el mismo objeto.

Características de la programación asincrónica

Los siguientes métodos síncronos tienen equivalentes basados en tareas que aceptan un token de cancelación:

Método síncrono Método asincrónico Example
SqlConnection.Open SqlConnection.OpenAsync Convertir un método síncrono
SqlCommand.ExecuteNonQuery SqlCommand.ExecuteNonQueryAsync Ejecutar comandos de forma asíncrona
SqlCommand.ExecuteScalar SqlCommand.ExecuteScalarAsync Ejecutar comandos de forma asíncrona
SqlCommand.ExecuteXmlReader SqlCommand.ExecuteXmlReaderAsync Ejecutar comandos de forma asíncrona
SqlCommand.ExecuteReader SqlCommand.ExecuteReaderAsync Convertir una secuencia de llamadas en código asíncrono
SqlDataReader.Read SqlDataReader.ReadAsync Convertir una secuencia de llamadas en código asíncrono
SqlDataReader.NextResult SqlDataReader.NextResultAsync Lee los resultados de forma asíncrona
SqlDataReader.IsDBNull SqlDataReader.IsDBNullAsync Lee los resultados de forma asíncrona
SqlDataReader.GetFieldValue<T> SqlDataReader.GetFieldValueAsync<T> Lee los resultados de forma asíncrona
SqlBatch.ExecuteNonQuery, ExecuteScalar y ExecuteReader SqlBatch.ExecuteNonQueryAsync, ExecuteScalarAsync y ExecuteReaderAsync Ejecutar un lote
SqlBulkCopy.WriteToServer SqlBulkCopy.WriteToServerAsync Utiliza operaciones asíncronas con SqlBulkCopy

Utiliza liberación síncrona cuando un objeto solo implementa IDisposable. Usa await using para objetos que implementan IAsyncDisposable. Libera el reader antes de reutilizar su conexión.

Ejecutar comandos de forma asíncrona

Los siguientes ejemplos asumen que connection está abierto. Usa ExecuteScalarAsync para un solo valor y pasa un token de cancelación propiedad del llamante:

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

Usa ExecuteNonQueryAsync para sentencias que cambian datos. Usa ExecuteXmlReaderAsync para una FOR XML consulta. Libera un lector XML antes de ejecutar otro comando en la misma conexión. Sin Multiple Active Result Sets (MARS), una conexión permite solo un lector abierto.

Leer los resultados de forma asíncrona

El siguiente ejemplo pasa el mismo token de cancelación a cada una de estas llamadas asíncronas:

  • ExecuteReaderAsync ejecuta la consulta.
  • ReadAsync Lee cada fila.
  • GetFieldValueAsync Lee el valor de cada columna.
  • NextResultAsync pasa al siguiente conjunto de resultados.
  • IsDBNullAsync busca 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 pasa al siguiente conjunto de resultados. GetFieldValueAsync y IsDBNullAsync leen una columna de la fila actual. Cuando usas el valor por defecto CommandBehavior, la fila ya está en búfer, por lo que estas llamadas suelen completarse sin esperar. Cuando usas CommandBehavior.SequentialAccess, pueden esperar a que lleguen los datos de la columna desde la red. Para más información sobre el acceso secuencial, consulte soporte para streaming de SqlClient.

Convertir un método síncrono

Este método síncrono abre una conexión, ejecuta un comando y devuelve el número de filas afectadas en 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()!;
}

Para convertir el método a código asincrónico, realiza estos cambios:

  • Cambia el tipo de retorno a Task<int>.
  • Añade un CancellationToken parámetro.
  • Sustituye Open por OpenAsync, y sustituye ExecuteScalar por ExecuteScalarAsync.
  • Elimina la conexión con await using.

El método convertido es así:

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

El llamante crea el token y espera el resultado:

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

Ambos métodos devuelven el mismo recuento. La salida depende de la base de datos a la que te conectes.

Connect Timeout Aun así, limita el establecimiento de conexiones y las esperas del pool. Cancelar mediante el token puede devolver antes el control al llamante.

Añadir llamadas asíncronas a una aplicación existente

Convierte una ruta de llamada completa a la vez. Evita llamar a .Result, .Wait() o GetAwaiter().GetResult() en tareas de base de datos porque esas llamadas bloquean y pueden provocar un interbloqueo en entornos con un contexto de sincronización.

No inicies un comando asincrónico y luego uses el mismo comando o lector de forma sincrónica antes de que termine la tarea. Sin MARS, no uses la misma conexión de forma sincrónica antes de que termine la tarea.

Añadir cancelación y límites de tiempo

Cancelación de una operación asincrónica

Pasar el mismo token de cancelación desde la apertura de la conexión hasta la ejecución 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 cancelación se realizará en la medida de lo posible. La operación puede terminar antes de que la cancelación entre en vigor, y la excepción observada depende de dónde ocurra la cancelación. No asumas que recibir una solicitud de cancelación significa que el servidor ha revertido el trabajo completado. Cuando un conjunto de cambios debe tener éxito o fallar como unidad, utiliza una transacción explícita. Para más información, consulte Transacciones locales.

Cancel solicita la cancelación del comando actual. No sustituye un token de cancelación propiedad del llamante. La operación podría terminar antes de que la solicitud de cancelación entre en vigor.

Tras la cancelación o el tiempo de espera, espera a que finalice la operación y libera el lector y el comando. Inspecciona el estado de la conexión antes de reutilizarla. Un fallo que rompa la conexión requiere una nueva conexión.

Configuración de tiempos de espera

Los tiempos de espera de conexión y comandos cubren diferentes fases:

Setting Portadas
Connect Timeout Establecimiento de conexión y espera una conexión en pool.
CommandTimeout Ejecución de comandos y esperas acumuladas de lectura de red.
Token de cancelación Una petición controlada por el llamante para dejar de esperar.

CommandTimeout se mide en segundos. 0 significa que no hay límite de tiempo para comandos. El valor predeterminado es el valor Command Timeout de la conexión cuando está configurada; de lo contrario, 30 segundos.

Para un lector, el tiempo de espera del comando se aplica a la espera acumulativa de red de cada operación de lectura del proveedor. No incluye el tiempo dedicado a procesar una fila en el código de la aplicación. El tiempo de espera puede agotarse antes de la primera fila o al recuperar una fila posterior.

Combinar cancelación y tiempo de espera de comando

Usa un tiempo límite de comandos y una cancelación por parte del llamador a la vez. Este ejemplo asume que connection está abierto:

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

Si el llamante cancela primero, la llamada que se está esperando lanza una excepción. El tipo de excepción depende de dónde ocurra la cancelación. Si el tiempo de espera del comando expira primero, el proveedor informa de un tiempo de espera. En cualquier caso, el comando puede terminar correctamente antes de que la cancelación o el tiempo de espera entre en vigor.

Patrones asíncronos avanzados

Utiliza métodos asíncronos a través de tipos de proveedores base

El código independiente del proveedor puede usar los métodos asíncronos DbConnection, DbCommand y DbDataReader. Crear parámetros mediante el comando provider y evitar downcasts a menos que la aplicación necesite una función específica de Microsoft.Data.SqlClient.

Utiliza métodos de transacción SQL basados en tareas

Utiliza los métodos de transacción local basados en tareas cuando el framework objetivo los exponga. El siguiente ejemplo asume que connection está abierto, sql contiene el texto del comando y el llamante pasa en 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 de limpieza aparte para la reversión. El token de operación podría ya estar cancelado. Gestionar un fallo de rollback sin descartar la excepción de operación original. Este ejemplo preserva ambas excepciones en una etiqueta AggregateException. Una aplicación puede en su lugar registrar la excepción de rollback y volver a lanzar la excepción original.

Estos métodos de Microsoft.Data.SqlClient utilizan las implementaciones heredadas DbConnection y DbTransaction, que llaman a los métodos de transacción síncronos y devuelven una tarea completada. Esperarlos no libera el hilo de llamada mientras se ejecuta la operación de transacción. La cancelación se comprueba antes de que comience la operación y no interrumpe una operación en curso. La ejecución de comandos dentro de la transacción sigue siendo asíncrona.

Utiliza transacciones distribuidas asíncronas

Los comandos asíncronos pueden ejecutarse dentro de un TransactionScope. El scope establece Transaction.Current, que .NET denomina transacción de ambiente. Una conexión que se abre dentro del ámbito se une automáticamente a esa transacción, porque la palabra clave de conexión Enlist por defecto es true. Configura TransactionScopeAsyncFlowOption.Enabled cuando debe fluir la transacción a través de await. Las transacciones distribuidas tienen requisitos de plataforma, servidor y coordinador. Para más información, consulte integración de System.Transactions.

Utiliza operaciones asíncronas con SqlBulkCopy

Use WriteToServerAsync para copia masiva asíncrona. Mantén activo el lector de origen o la colección de filas hasta que se complete la tarea devuelta. Configura el tamaño del lote, el comportamiento de las transacciones y el tiempo de espera para la operación. Para más información, véase Operaciones de copia masiva.

Uso asincrónico de varios comandos con MARS

MARS permite más de un comando pendiente en una misma conexión física. Actívalo MultipleActiveResultSets=True solo cuando la aplicación necesite ese modelo de compartición de conexión.

MARS no es necesario para la programación asíncrona. Sin MARS, debe terminar o desechar el lector activo antes de ejecutar otro comando en la misma conexión. Una segunda conexión suele ser más clara para trabajos verdaderamente independientes.

Para el comportamiento de las sesiones y las restricciones de transacciones, consulte Habilitar Múltiples Conjuntos de Resultados Activos.

Lectura y actualización de datos de manera asincrónica con MARS

Cuando activas MARS, un comando puede leer filas mientras otro comando se ejecuta en la misma conexión. Espera cada comando y cada operación de lector por separado. Las operaciones MARS no son seguras para subprocesos, y la ejecución del servidor aún puede serializarse en los puntos de cesión.

Cuando las operaciones de lectura y actualización deben ser atómicas, asigne la misma transacción local a cada comando y pruebe el comportamiento MARS de la transacción. No confirmes mientras otra operación en esa transacción sigue pendiente.

Programación asincrónica heredada

Microsoft. Data.SqlClient conserva los métodos BeginExecuteNonQuerydel Modelo de Programación Asíncrona (APM) , BeginExecuteReader, y BeginExecuteXmlReader para la compatibilidad. No los uses en código nuevo. CommandTimeout es ignorado por estas llamadas asíncronas de estilo antiguo y se respeta mediante métodos basados en tareas como ExecuteReaderAsync.

Los métodos heredados no requieren Asynchronous Processing=true. Microsoft. Data.SqlClient 4.0 y versiones posteriores no soportan la Asynchronous Processing propiedad de conexión.