C# – Utilisation des transactions SQL avec Dapper

C# – Utilisation des transactions SQL avec Dapper

L'utilisation de TransactionScope est le moyen le plus simple d'exécuter plusieurs commandes SQL dans une transaction. Voici un exemple d'utilisation :

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)

Lorsque vous appelez TransactionScope.Complete(), il valide la transaction. Si vous n'appelez pas Complete(), il annulera la transaction une fois qu'elle aura quitté le bloc TransactionScope.

Cela permet de garder le code agréable et propre et fonctionne bien avec le modèle Repository. Voici les méthodes Insert()/Delete() dans la classe du référentiel. Remarquez qu'il n'a pas du tout à gérer les transactions ?

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)

Remarque :Utilisation de .NET 5 avec une base de données SQL Server 2016.

Toute connexion ouverte dans le bloc TransactionScope est automatiquement inscrite dans la transaction.

Transactions distribuées

Lorsqu'une transaction est créée, elle commence comme une transaction locale. Sous certaines conditions, elle est transmise à une transaction distribuée qui nécessite le Coordinateur de transactions distribuées (MSDTC) service à exécuter. Il existe deux conditions principales qui entraînent l'escalade des transactions :

  • Ouverture explicite de deux connexions dans la portée de la transaction en même temps.
  • Utiliser différentes chaînes de connexion (par exemple, si vous vous connectez à un autre serveur).

Le moteur de base de données / la version que vous utilisez joue également un rôle. Il est préférable de déterminer tôt dans le processus de développement si vous allez devoir gérer ou non des transactions distribuées. C'est parce qu'ils peuvent être un obstacle architectural. Idéalement, essayez d'éviter les transactions distribuées.

Transactions distribuées non prises en charge dans .NET Core

Les transactions distribuées ne sont actuellement pas prises en charge dans les versions multiplateformes de .NET (.NET Core et versions ultérieures). Il est possible que Microsoft en ajoute éventuellement la prise en charge. Lorsque vous faites quelque chose qui déclenche une escalade de transaction, vous obtenez l'exception suivante :

Si vous migrez vers .NET Core et que vous avez besoin de transactions distribuées, il s'agit d'un obstacle majeur qui nécessiterait une refonte pour éliminer le besoin de transactions distribuées.

Remarque :Vous pouvez obtenir l'erreur "MSDTC n'est pas disponible" si le service MSDTC ne s'exécute pas, ce qui prête à confusion car il n'est pas pertinent. Si MSDTC est en cours d'exécution, vous obtiendrez l'exception "plate-forme non prise en charge".

Transactions distribuées dans .NET Framework

Les transactions distribuées sont prises en charge dans .NET Framework et nécessitent le Coordinateur de transactions distribuées (MSDTC) service à exécuter. Lorsqu'une transaction est escaladée et que le service MSDTC n'est pas en cours d'exécution, vous obtenez l'erreur :

Assurez-vous que le service MSDTC est en cours d'exécution et configuré pour démarrer automatiquement.

Éviter l'escalade des transactions lors de la connexion à différentes bases de données sur le même serveur

Différentes chaînes de connexion déclenchent l'escalade des transactions, même si vous vous connectez à différentes bases de données sur le même serveur. Par exemple, le code suivant (exécuté dans un TransactionScope) déclenche une escalade de transaction et échoue avec l'exception "plate-forme non prise en charge" (dans .NET multiplateforme) :

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)

Une façon d'éviter l'escalade de la transaction consiste à utiliser la même chaîne de connexion et à basculer vers la base de données cible avec 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)

Comme il s'agit de la même chaîne de connexion, la transaction n'est pas escaladée.

Remarque :C'est la même chose que d'appeler con.Open() + con.ChangeDatabase("MoviesDbOld"), juste plus simple car je préfère laisser Dapper ouvrir la connexion.

Alternative à TransactionScope - Connection.BeginTransaction()

Si vous préférez avoir un contrôle plus explicite sur la transaction, vous pouvez utiliser le style Connection.BeginTransaction() au lieu d'utiliser TransactionScope. Voici un exemple :

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)

Une chose à noter est que la connexion doit être ouverte avant d'appeler BeginTransaction().

Si vous n'appelez pas Commit(), il annulera automatiquement la transaction lorsqu'elle quittera le bloc BeginTransaction using. Vous pouvez également appeler Rollback() vous-même.