TransactionScope en .NET
Qu’est-ce que c’est ?
TransactionScope est une classe du namespace System.Transactions qui permet de définir un bloc de code transactionnel de manière déclarative. Toutes les opérations effectuées à l’intérieur du bloc (requêtes SQL, appels à plusieurs bases de données, etc.) sont soit toutes validées (commit), soit toutes annulées (rollback) en cas d’erreur.
C’est l’approche recommandée quand on veut garantir la cohérence des données sans gérer manuellement les objets SqlTransaction de chaque connexion.
Fonctionnement de base
using (var scope = new TransactionScope())
{
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
// Exécution de requêtes SQL...
}
// Éventuellement une deuxième connexion, une autre base, etc.
scope.Complete(); // Valide la transaction
}
Points clés
- Si
scope.Complete()n’est pas appelé avant la fin duusing, la transaction est automatiquement annulée (rollback). TransactionScopes’appuie sur le gestionnaire de transactions ambiant (Transaction.Current). Toute ressource compatible (ADO.NET, Entity Framework, MSMQ…) qui s’exécute dans ce contexte rejoint automatiquement la transaction en cours, sans code supplémentaire.- Si plusieurs connexions à des bases différentes sont ouvertes dans le même scope, .NET peut escalader automatiquement vers une transaction distribuée gérée par le DTC (Distributed Transaction Coordinator).
Les options de configuration
var options = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(30)
};
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
// ...
scope.Complete();
}
TransactionScopeOption
| Valeur | Comportement |
|---|---|
Required (par défaut) |
Rejoint une transaction existante si elle existe, sinon en crée une nouvelle |
RequiresNew |
Crée toujours une nouvelle transaction, indépendante de la transaction ambiante |
Suppress |
Exécute le code sans transaction, même si on est dans un scope englobant |
IsolationLevel
Définit le niveau d’isolation vis-à-vis des autres transactions concurrentes.
Le choix impacte directement les performances et les risques de verrous (locks).
| Niveau d’isolation | Comportement principal | Risques / Effets de bord évités ou possibles | Impact performance |
|---|---|---|---|
ReadUncommitted |
Permet de lire des données modifiées mais non encore validées par d’autres transactions. | Risques : Lectures sales (Dirty Reads), non-répétables et fantômes. | Très faible (pas de verrou de lecture). |
ReadCommitted (par défaut) |
Empêche la lecture de données non validées (évite les lectures sales). | Évite : Lectures sales. Risques : Lectures non-répétables et fantômes. |
Équilibré (verrous posés et relâchés rapidement). |
RepeatableRead |
Garantit que les données lues ne pourront pas être modifiées par d’autres transactions tant qu’elles ne sont pas libérées. | Évite : Lectures sales et non-répétables. Risques : Lectures fantômes. |
Modéré à élevé (verrous maintenus plus longtemps). |
Serializable |
Le niveau le plus strict. Simule l’exécution des transactions de manière totalement séquentielle. | Évite : Tous les problèmes de concurrence (sales, non-répétables, fantômes). | Élevé (fort risque de blocages/conflits et baisse de débit). |
Snapshot |
Lit une version cohérente des données telle qu’elle existait au début de la transaction (via le versioning de lignes). | Évite : Lectures sales, non-répétables et fantômes sans bloquer les écritures. | Modéré (consomme de l’espace pour stocker les versions de lignes). |
Unspecified |
Aucun niveau d’isolation n’est spécifié. | Utilise généralement le niveau par défaut du stockage sous-jacent. | Variable selon le fournisseur. |
Version asynchrone (important !)
Avec async/await, il faut impérativement utiliser le constructeur avec TransactionScopeAsyncFlowOption.Enabled, sinon le contexte transactionnel ne “traverse” pas les changements de thread liés à l’asynchronisme :
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
TransactionScopeAsyncFlowOption.Enabled))
{
await connection.OpenAsync();
// ...
scope.Complete();
}
⚠️ Oublier ce paramètre est une erreur très fréquente et peut provoquer des TransactionAbortedException ou des comportements incohérents.
Points d’attention
- Durée du scope : plus une transaction reste ouverte longtemps, plus elle bloque de ressources (verrous en base). Il faut garder le bloc le plus court possible.
- Escalade DTC : ouvrir deux connexions distinctes (même vers la même base de données selon les cas) peut déclencher une escalade vers le DTC, plus coûteuse et nécessitant que le service MSDTC soit activé sur le serveur.
- Exceptions : toute exception non interceptée dans le scope empêche l’appel à
Complete(), ce qui déclenche un rollback automatique — c’est le comportement souhaité dans la majorité des cas. - Imbrication : des
TransactionScopepeuvent être imbriqués ; le comportement dépend deTransactionScopeOptionchoisi pour le scope enfant.
Exemple complet avec gestion d’erreur
public void TransfererFonds(int compteSourceId, int compteDestId, decimal montant)
{
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
TransactionScopeAsyncFlowOption.Enabled))
{
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
DebiterCompte(connection, compteSourceId, montant);
CrediterCompte(connection, compteDestId, montant);
}
scope.Complete();
}
// Si une exception survient avant Complete(), tout est annulé automatiquement.
}
En résumé
| Avantage | Inconvénient |
|---|---|
| Code déclaratif, simple à lire | Peut masquer des transactions coûteuses (escalade DTC) |
| Fonctionne avec plusieurs technologies (ADO.NET, EF, MSMQ) | Nécessite rigueur sur l’option async |
Rollback automatique en cas d’oubli de Complete() |
Verrous en base si le scope est trop long |