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 cada operación antes de volver a utilizar 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.

Utilizar await desde la apertura de la conexión hasta el procesamiento de los 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 un resultado cuando el primero está disponible, no cuando llegan 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 una lectura asíncrona, una transición de resultado o una operación de campo pendiente en 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 la liberación síncrona cuando un objeto solo implemente IDisposable. Usa await using para objetos que implementan IAsyncDisposable. Libera el lector 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 comprueba si hay 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 se utiliza CommandBehavior.SequentialAccess, pueden esperar a recibir los datos de las columnas 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 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 sigue limitando el establecimiento de conexiones y las esperas del grupo de conexiones. La cancelación del token puede devolver el control al llamante antes.

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, ya que esas llamadas se bloquean y pueden provocar interbloqueos 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 realiza 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 agotado, 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 la conexión y esperas para una conexión del grupo.
CommandTimeout Ejecución de comandos y esperas acumuladas de lectura de red.
Token de cancelación Una solicitud 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á configurado; de lo contrario, son 30 segundos.

En el caso de un lector, el tiempo de espera del comando se aplica a la espera de red acumulada 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

Utilice conjuntamente un tiempo de espera de comando finito y la cancelación por parte del llamante. 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;
}

Utilice un token de limpieza independiente 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 conserva ambas excepciones en un 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 ámbito establece Transaction.Current, lo que .NET denomina transacción ambiental. Una conexión que se abre dentro del ámbito se inscribe automáticamente en esa transacción, ya que la palabra clave de conexión Enlist tiene como valor predeterminado true. Configure TransactionScopeAsyncFlowOption.Enabled cuando la transacción deba extenderse 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

Utiliza WriteToServerAsync para la 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 ceda el paso.

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 realices el commit mientras otra operación de esa transacción siga 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.