C#:uso de transacciones SQL con Dapper

C#:uso de transacciones SQL con Dapper

El uso de TransactionScope es la forma más sencilla de ejecutar varios comandos SQL en una transacción. Aquí hay un ejemplo de cómo usarlo:

using System.Transactions;

using (var trxScope = new TransactionScope())
{
	movieRepo.Insert(newMovie);
	movieRepo.Delete(movieToDelete);

	//Commits the transaction
	trxScope.Complete();
};
//Rolls back the transaction if Complete() wasn't called
Code language: C# (cs)

Cuando llama a TransactionScope.Complete(), confirma la transacción. Si no llama a Complete(), revertirá la transacción una vez que abandone el bloque TransactionScope.

Esto mantiene el código agradable y limpio y funciona bien con el patrón Repositorio. Aquí están los métodos Insert()/Delete() en la clase de repositorio. ¿Te das cuenta de que no tiene que lidiar con transacciones en absoluto?

using Dapper;
using System.Data.SqlClient;

public class MovieRepository
{
	public void Insert(Movie movie)
	{
		using (var con = new SqlConnection(connectionString))
		{
			con.Execute(INSERT_SQL, param: movie);
		}
	}
	public void Delete(Movie movie)
	{
		using (var con = new SqlConnection(connectionString))
		{
			con.Execute(DELETE_SQL,
				param: new { id = movie.Id });
		}
	}
	//rest of class
}
Code language: C# (cs)

Nota:uso de .NET 5 contra una base de datos de SQL Server 2016.

Cualquier conexión que se abra dentro del bloque TransactionScope se incluye automáticamente en la transacción.

Transacciones distribuidas

Cuando se crea una transacción, comienza como una transacción local. Bajo ciertas condiciones, se escala a una transacción distribuida que requiere el Coordinador de transacciones distribuidas (MSDTC) servicio que se está ejecutando. Hay dos condiciones principales que hacen que las transacciones aumenten:

  • Abrir explícitamente dos conexiones en el ámbito de la transacción al mismo tiempo.
  • Utilizar diferentes cadenas de conexión (como si se estuviera conectando a un servidor diferente).

El motor/versión de la base de datos que está utilizando también juega un papel. Lo mejor es averiguar al principio del proceso de desarrollo si tendrá que lidiar con transacciones distribuidas o no. Esto se debe a que pueden ser un obstáculo arquitectónico. Lo ideal es intentar evitar las transacciones distribuidas.

Transacciones distribuidas no admitidas en .NET Core

Actualmente, las transacciones distribuidas no son compatibles con las versiones multiplataforma de .NET (.NET Core y superior). Es posible que Microsoft agregue soporte para esto eventualmente. Cuando hace algo que activa la escalada de transacciones, obtiene la siguiente excepción:

Si está migrando a .NET Core y necesita transacciones distribuidas, este es un bloqueador importante que requeriría un rediseño para eliminar la necesidad de transacciones distribuidas.

Nota:Es posible que obtenga el error "MSDTC no está disponible" si el servicio MSDTC no se está ejecutando, lo cual es confuso porque es irrelevante. Si se está ejecutando MSDTC, obtendrá la excepción "plataforma no compatible".

Transacciones distribuidas en .NET Framework

Las transacciones distribuidas son compatibles con .NET Framework y requieren el Coordinador de transacciones distribuidas (MSDTC) servicio que se está ejecutando. Cuando se escala una transacción y no tiene el servicio MSDTC ejecutándose, obtiene el error:

Asegúrese de que el servicio MSDTC se esté ejecutando y configurado para iniciarse automáticamente.

Evitar la escalada de transacciones al conectarse a diferentes bases de datos en el mismo servidor

Diferentes cadenas de conexión activan la escalada de transacciones, incluso si se está conectando a diferentes bases de datos en el mismo servidor. Por ejemplo, el siguiente código (ejecutado dentro de un TransactionScope) activa la escalada de la transacción y falla con la excepción "plataforma no admitida" (en .NET multiplataforma):

public void Insert(Movie movie)
{
	using (var con = new SqlConnection("Server=MAKOLYTE;Database=MoviesDbNew;Integrated Security=true"))
	{
		con.Execute(INSERT_SQL, param: movie);
	}
}
public void Delete(Movie movie)
{
	using (var con = new SqlConnection("Server=MAKOLYTE;Database=MoviesDbOld;Integrated Security=true"))
	{
		con.Execute(DELETE_SQL,
			param: new { id = movie.Id });
}
Code language: C# (cs)

Una forma de evitar la escalada de transacciones es usar la misma cadena de conexión y cambiar a la base de datos de destino con USE :

public void Insert(Movie movie)
{
	using (var con = new SqlConnection("Server=MAKOLYTE;Database=MoviesDbNew;Integrated Security=true"))
	{
		con.Execute(INSERT_SQL, param: movie);
	}
}
public void Delete(Movie movie)
{
	using (var con = new SqlConnection("Server=MAKOLYTE;Database=MoviesDbNew;Integrated Security=true"))
	{
		con.Execute("USE MoviesDbOld");
		con.Execute(DELETE_SQL,
			param: new { id = movie.Id });
	}
}
Code language: C# (cs)

Debido a que es la misma cadena de conexión, no escala la transacción.

Nota:Esto es lo mismo que llamar a con.Open() + con.ChangeDatabase(“MoviesDbOld”), solo que más simple porque prefiero dejar que Dapper abra la conexión.

Alternativa a TransactionScope:Connection.BeginTransaction()

Si prefiere tener un control más explícito sobre la transacción, puede usar el estilo Connection.BeginTransaction() en lugar de usar TransactionScope. He aquí un ejemplo:

using Dapper;
using System.Data.SqlClient;

using(var con = new SqlConnection(connectionString))
{
	con.Open();
	using(var trx= con.BeginTransaction())
	{
		con.Execute(INSERT_SQL, param: movieToInsert, transaction: trx);
		con.Execute(DELETE_SQL, param: new { movieToDelete.Id }, transaction: trx);

		trx.Commit();
	}
}
Code language: C# (cs)

Una cosa a tener en cuenta es que la conexión debe abrirse antes de llamar a BeginTransaction().

Si no llama a Commit(), revertirá la transacción automáticamente cuando abandone el bloque BeginTransaction usando. También puede llamar a Rollback() usted mismo.