Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
La TransactionScope classe fournit un moyen simple de marquer un bloc de code comme participant à une transaction, sans avoir à interagir avec la transaction elle-même. Une étendue de transaction peut sélectionner et gérer automatiquement la transaction ambiante. En raison de sa facilité d’utilisation et de son efficacité, il est recommandé d’utiliser la TransactionScope classe lors du développement d’une application transactionnelle.
En outre, vous n’avez pas besoin d’inscrire explicitement des ressources dans la transaction. N'importe quel System.Transactions gestionnaire de ressources (par exemple, SQL Server 2005) peut détecter l'existence d'une transaction en cours créée par le contexte et s'inscrire automatiquement.
Création d’un périmètre de transaction
L’exemple suivant montre une utilisation simple de la TransactionScope classe.
// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Initialize the return value to zero and create a StringWriter to display results.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
try
{
// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// If you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources as connection2 is opened
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// The transaction is escalated to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
}
// The Complete method commits the transaction. If an exception has been thrown,
// Complete is not called and the transaction is rolled back.
scope.Complete();
}
}
catch (TransactionAbortedException ex)
{
writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}
' This function takes arguments for 2 connection strings and commands to create a transaction
' involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
' transaction is rolled back. To test this code, you can connect to two different databases
' on the same server by altering the connection string, or to another 3rd party RDBMS
' by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
ByVal connectString1 As String, ByVal connectString2 As String, _
ByVal commandText1 As String, ByVal commandText2 As String) As Integer
' Initialize the return value to zero and create a StringWriter to display results.
Dim returnValue As Integer = 0
Dim writer As System.IO.StringWriter = New System.IO.StringWriter
Try
' Create the TransactionScope to execute the commands, guaranteeing
' that both commands can commit or roll back as a single unit of work.
Using scope As New TransactionScope()
Using connection1 As New SqlConnection(connectString1)
' Opening the connection automatically enlists it in the
' TransactionScope as a lightweight transaction.
connection1.Open()
' Create the SqlCommand object and execute the first command.
Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
returnValue = command1.ExecuteNonQuery()
writer.WriteLine("Rows to be affected by command1: {0}", returnValue)
' If you get here, this means that command1 succeeded. By nesting
' the using block for connection2 inside that of connection1, you
' conserve server and network resources as connection2 is opened
' only when there is a chance that the transaction can commit.
Using connection2 As New SqlConnection(connectString2)
' The transaction is escalated to a full distributed
' transaction when connection2 is opened.
connection2.Open()
' Execute the second command in the second database.
returnValue = 0
Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
returnValue = command2.ExecuteNonQuery()
writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
End Using
End Using
' The Complete method commits the transaction. If an exception has been thrown,
' Complete is called and the transaction is rolled back.
scope.Complete()
End Using
Catch ex As TransactionAbortedException
writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
End Try
' Display messages.
Console.WriteLine(writer.ToString())
Return returnValue
End Function
L'étendue de transaction démarre après la création d'un nouvel objet TransactionScope. Comme illustré dans l’exemple de code, il est recommandé de créer des étendues avec une using
instruction. L’instruction using
est disponible à la fois en C# et en Visual Basic, et fonctionne comme un bloc try
...finally
afin de garantir que la portée est gérée correctement.
Lorsque vous instanciez TransactionScope, le gestionnaire de transactions détermine la transaction à laquelle participer. Une fois déterminé, le périmètre participe toujours à cette transaction. La décision est basée sur deux facteurs : si une transaction ambiante est présente et la valeur du TransactionScopeOption
paramètre dans le constructeur. La transaction ambiante est la transaction dans laquelle votre code s’exécute. Vous pouvez obtenir une référence à la transaction ambiante en appelant la propriété statique Transaction.Current de la Transaction classe. Pour plus d’informations sur l’utilisation de ce paramètre, consultez la section Gestion du flux de transactions à l’aide de TransactionScopeOption de cette rubrique.
Fin d'une étendue de transaction
Lorsque votre application termine tout le travail qu’elle souhaite effectuer dans une transaction, vous devez appeler la TransactionScope.Complete méthode une seule fois pour informer le gestionnaire de transactions qu’il est acceptable de valider la transaction. C’est une très bonne pratique que de placer l’appel à Complete comme dernière instruction du bloc using
.
L’échec de l’appel de cette méthode abandonne la transaction, car le gestionnaire de transactions l’interprète comme une défaillance système ou équivaut à une exception levée dans l’étendue de la transaction. Toutefois, l’appel de cette méthode ne garantit pas que la transaction sera validée. Il s’agit simplement d’informer le gestionnaire de transactions de votre statut. Après avoir appelé la méthode Complete, vous ne pouvez plus accéder à la transaction ambiante à l’aide de la propriété Current, et tenter de le faire entraîne le déclenchement d'une exception.
Si l’objet TransactionScope a créé la transaction initialement, le travail réel de validation de la transaction par le gestionnaire de transactions se produit après la dernière ligne de code dans le using
bloc. S’il n’a pas créé la transaction, la validation se produit chaque fois qu’elle Commit est appelée par le propriétaire de l’objet CommittableTransaction . À ce stade, le gestionnaire de transactions appelle les gestionnaires de ressources et les informe de valider ou d'annuler, selon que la méthode Complete a été appelée sur l'objet TransactionScope.
L’instruction using
garantit que la Dispose méthode de l’objet TransactionScope est appelée même si une exception se produit. La Dispose méthode marque la fin de l’étendue de transaction. Les exceptions qui se produisent après l’appel de cette méthode peuvent ne pas affecter la transaction. Cette méthode restaure également la transaction ambiante à son état précédent.
Une exception TransactionAbortedException est levée si l'étendue crée la transaction et que cette transaction est abandonnée. Une TransactionInDoubtException est levée si le gestionnaire de transactions ne peut pas arriver à une décision de validation. Aucune exception n'est levée si la transaction est validée.
Restauration d’une transaction
Si vous souhaitez annuler une transaction, vous ne devez pas appeler la méthode Complete dans l'étendue de la transaction. Par exemple, vous pouvez lever une exception dans l'étendue. La transaction à laquelle elle participe sera annulée.
Gestion du flux de transactions à l’aide de TransactionScopeOption
L'étendue de transaction peut être imbriquée en appelant une méthode qui utilise une TransactionScope à partir d'une méthode utilisant sa propre étendue, comme la méthode RootMethod
de l'exemple suivant,
void RootMethod()
{
using(TransactionScope scope = new TransactionScope())
{
/* Perform transactional work here */
SomeMethod();
scope.Complete();
}
}
void SomeMethod()
{
using(TransactionScope scope = new TransactionScope())
{
/* Perform transactional work here */
scope.Complete();
}
}
L’étendue de transaction la plus haute est appelée étendue racine.
La TransactionScope classe fournit plusieurs constructeurs surchargés qui acceptent une énumération du type TransactionScopeOption, qui définit le comportement transactionnel de l’étendue.
Un TransactionScope objet a trois options :
Joignez la transaction ambiante ou créez-en une si elle n’existe pas.
Constituer une nouvelle étendue racine, c'est-à-dire démarrer une nouvelle transaction et en faire la nouvelle transaction ambiante de sa propre étendue.
Ne participez pas à une transaction du tout. Il n’existe donc aucune transaction ambiante.
Si l'étendue est instanciée avec Required et qu'une transaction ambiante existe, l'étendue joint cette transaction. Si, d’autre part, il n’existe aucune transaction ambiante, l’étendue crée une nouvelle transaction et devient l’étendue racine. Il s’agit de la valeur par défaut. Si Required est utilisé, le code de l'étendue n'a pas à se comporter différemment selon qu'il s'agit de la racine ou seulement de la jonction de la transaction ambiante. Elle doit fonctionner de façon identique dans les deux cas.
Si l’étendue est instanciée avec RequiresNew, il s’agit toujours de l’étendue racine. Une nouvelle transaction démarre et devient la nouvelle transaction ambiante de l'étendue.
Si la portée est instanciée avec Suppress, elle ne prend jamais part à une transaction, qu'une transaction ambiante existe ou non. Une étendue instanciée avec cette valeur a toujours null
pour sa transaction ambiante.
Les options ci-dessus sont résumées dans le tableau suivant.
TransactionScopeOption | Transaction ambiante | L'étendue participe à |
---|---|---|
Obligatoire | Non | Nouvelle transaction (future racine) |
Nécessite un nouveau | Non | Nouvelle transaction (future racine) |
Supprimer | Non | Aucune transaction |
Obligatoire | Oui | Transaction ambiante |
Nécessite un nouveau | Oui | Nouvelle transaction (future racine) |
Supprimer | Oui | Aucune transaction |
Lorsqu'un objet TransactionScope joint une transaction ambiante existante, la suppression de l'objet d'étendue peut ne pas entraîner l'arrêt de la transaction, à moins que l'étendue abandonne la transaction. Si la transaction ambiante a été créée par une étendue racine, Commit est uniquement appelée sur la transaction lorsque l’étendue racine est supprimée. Si la transaction a été créée manuellement, la transaction se termine lorsqu’elle est abandonnée ou validée par son créateur.
L’exemple suivant montre un TransactionScope objet qui crée trois objets de portée imbriqués, chacun instancié avec une valeur TransactionScopeOption différente.
using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
{
//...
}
using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
{
//...
}
using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
{
//...
}
}
L’exemple montre un bloc de code sans transaction ambiante créant une nouvelle étendue (scope1
) avec Required. L’étendue scope1
est une étendue racine, car elle crée une nouvelle transaction (Transaction A) et fait de la Transaction A la transaction ambiante.
Scope1
crée ensuite trois objets supplémentaires, chacun avec une valeur différente TransactionScopeOption . Par exemple, scope2
est créé avec Required, et étant donné qu’il existe une transaction ambiante, il joint la première transaction créée par scope1
. Notez que scope3
est l'étendue racine d'une nouvelle transaction et que scope4
n'a pas de transaction ambiante.
Bien que la valeur TransactionScopeOption par défaut et la plus couramment utilisée soit Required, chacune des autres valeurs a son objectif unique.
Code non transactionnel à l’intérieur d’une étendue de transaction
Suppress est utile lorsque vous souhaitez conserver les opérations effectuées par la section de code et ne souhaitez pas abandonner la transaction ambiante si les opérations échouent. Pour effectuer des opérations d'enregistrement ou d'audit par exemple, ou pour publier des événements aux abonnés, indépendamment de la validation ou de l'abandon de votre transaction ambiante. Cette valeur vous permet d’avoir une section de code non transactionnelle à l’intérieur d’une étendue de transaction, comme illustré dans l’exemple suivant.
using(TransactionScope scope1 = new TransactionScope())
{
try
{
//Start of non-transactional section
using(TransactionScope scope2 = new
TransactionScope(TransactionScopeOption.Suppress))
{
//Do non-transactional work here
}
//Restores ambient transaction here
}
catch {}
//Rest of scope1
}
Vote au sein d'une étendue imbriquée
Bien qu’une étendue imbriquée puisse joindre la transaction ambiante de l’étendue racine, l’appel de Complete dans l’étendue imbriquée n’a pas d’effet sur l’étendue racine. La transaction est validée uniquement si toutes les étendues, de l’étendue racine jusqu’à la dernière étendue imbriquée, votent pour valider la transaction. Le non-appel de Complete dans une étendue imbriquée affecte l’étendue racine, car la transaction ambiante est immédiatement abandonnée.
Définir le délai d'expiration de TransactionScope
Certains constructeurs surchargés de TransactionScope acceptent une valeur de type TimeSpan, qui est utilisée pour contrôler le délai d'expiration de la transaction. Un délai d’attente défini sur zéro signifie un délai d’expiration infini. Le délai d’expiration infini est utile principalement pour le débogage, lorsque vous souhaitez isoler un problème dans votre logique métier en parcourant votre code, et que vous ne souhaitez pas que la transaction que vous déboguez expire pendant que vous tentez de localiser le problème. Soyez extrêmement prudent à l’aide de la valeur de délai d’expiration infinie dans tous les autres cas, car elle remplace les protections contre les blocages de transaction.
Vous définissez généralement le TransactionScope délai d’expiration sur des valeurs autres que la valeur par défaut dans deux cas. La première est au cours du développement, lorsque vous souhaitez tester la façon dont votre application gère les transactions abandonnées. En définissant le délai d’expiration sur une petite valeur (par exemple, une milliseconde), vous provoquez l’échec de votre transaction et pouvez ainsi observer votre code de gestion des erreurs. Le second cas pour lequel il est nécessaire de définir une valeur inférieure au délai d'attente par défaut est lorsque vous pensez que l'étendue est à l'origine de conflits de ressources, causant des blocages. Dans ce cas, vous souhaitez abandonner la transaction dès que possible et ne pas attendre l’expiration du délai d’expiration par défaut.
Lorsqu’une étendue joint une transaction ambiante, mais spécifie un délai d’expiration plus petit que celui sur lequel la transaction ambiante est définie, le nouveau délai d’attente plus court est appliqué à l’objet TransactionScope et l’étendue doit se terminer dans le délai imbriqué spécifié, ou la transaction est automatiquement abandonnée. Si le délai d'attente de l'étendue imbriquée est supérieur à celui de la transaction ambiante, il n'a aucun effet.
Définition du niveau d’isolation TransactionScope
Certains des constructeurs surchargés de TransactionScope acceptent une structure de type TransactionOptions pour spécifier un niveau d'isolation, en plus d'une valeur de délai d'attente. Par défaut, la transaction s’exécute avec le niveau d’isolation défini sur Serializable. Sélectionner un niveau d'isolation autre que Serializable est couramment utilisé pour les systèmes gourmands en lecture. Cela nécessite une solide compréhension de la théorie du traitement des transactions et de la sémantique de la transaction elle-même, des problèmes d’accès concurrentiel impliqués et des conséquences pour la cohérence du système.
En outre, tous les gestionnaires de ressources ne prennent pas en charge tous les niveaux d’isolation et peuvent choisir de participer à la transaction à un niveau supérieur à celui configuré.
Chaque niveau d’isolation en plus Serializable est susceptible d’incohérence résultant d’autres transactions accédant aux mêmes informations. La différence entre les différents niveaux d’isolation est la façon dont les verrous de lecture et d’écriture sont utilisés. Un verrou ne peut être conservé que lorsque la transaction accède aux données dans le gestionnaire de ressources, ou qu’elle peut être conservée jusqu’à ce que la transaction soit validée ou abandonnée. L’ancien est préférable pour le débit, celui-ci pour la cohérence. Les deux types de verrous et les deux types d’opérations (lecture/écriture) donnent quatre niveaux d’isolation de base. Pour plus d’informations, consultez IsolationLevel.
Lorsque vous utilisez des objets imbriqués TransactionScope, toutes les étendues imbriquées doivent être configurées pour utiliser exactement le même niveau d’isolation si elles souhaitent rejoindre la transaction ambiante. Si un objet TransactionScope imbriqué tente de joindre la transaction ambiante avec un niveau d'isolation différent, une exception ArgumentException est levée.
Interopérabilité avec COM+
Lorsque vous créez une TransactionScope instance, vous pouvez utiliser l’énumération EnterpriseServicesInteropOption dans l’un des constructeurs pour spécifier comment interagir avec COM+. Pour plus d’informations sur ce problème, consultez Interopérabilité avec les services d’entreprise et les transactions COM+.