¿En qué circunstancias se conecta automáticamente una SqlConnection en una Transacción TransactionScope ambiental?

¿Qué significa que una conexión Sql se “alistó” en una transacción? ¿Significa simplemente que los comandos que ejecuto en la conexión participarán en la transacción?

Si es así, ¿bajo qué circunstancias se conecta automáticamente una SqlConnection en una transacción de TransactionScope ambiental?

Ver preguntas en comentarios del código. Mi suposición de la respuesta de cada pregunta sigue cada pregunta entre paréntesis.

Escenario 1: apertura de conexiones DENTRO de un scope de transacción

using (TransactionScope scope = new TransactionScope()) using (SqlConnection conn = ConnectToDB()) { // Q1: Is connection automatically enlisted in transaction? (Yes?) // // Q2: If I open (and run commands on) a second connection now, // with an identical connection string, // what, if any, is the relationship of this second connection to the first? // // Q3: Will this second connection's automatic enlistment // in the current transaction scope cause the transaction to be // escalated to a distributed transaction? (Yes?) } 

Escenario 2: Usar conexiones DENTRO de un ámbito de transacción que se abrió FUERA de él

 //Assume no ambient transaction active now SqlConnection new_or_existing_connection = ConnectToDB(); //or passed in as method parameter using (TransactionScope scope = new TransactionScope()) { // Connection was opened before transaction scope was created // Q4: If I start executing commands on the connection now, // will it automatically become enlisted in the current transaction scope? (No?) // // Q5: If not enlisted, will commands I execute on the connection now // participate in the ambient transaction? (No?) // // Q6: If commands on this connection are // not participating in the current transaction, will they be committed // even if rollback the current transaction scope? (Yes?) // // If my thoughts are correct, all of the above is disturbing, // because it would look like I'm executing commands // in a transaction scope, when in fact I'm not at all, // until I do the following... // // Now enlisting existing connection in current transaction conn.EnlistTransaction( Transaction.Current ); // // Q7: Does the above method explicitly enlist the pre-existing connection // in the current ambient transaction, so that commands I // execute on the connection now participate in the // ambient transaction? (Yes?) // // Q8: If the existing connection was already enlisted in a transaction // when I called the above method, what would happen? Might an error be thrown? (Probably?) // // Q9: If the existing connection was already enlisted in a transaction // and I did NOT call the above method to enlist it, would any commands // I execute on it participate in it's existing transaction rather than // the current transaction scope. (Yes?) } 

He hecho algunas pruebas desde que hice esta pregunta y encontré la mayoría, si no todas, las respuestas por mi cuenta, ya que nadie más respondió. Por favor, avíseme si me he perdido algo.

Q1. Sí, a menos que “enlist = false” esté especificado en la cadena de conexión. El grupo de conexiones encuentra una conexión utilizable. Una conexión utilizable es aquella que no se alista en una transacción o que se alistó en la misma transacción.

Q2. La segunda conexión es una conexión independiente, que participa en la misma transacción. No estoy seguro de la interacción de los comandos en estas dos conexiones, ya que se ejecutan en la misma base de datos, pero creo que pueden ocurrir errores si se emiten comandos en ambos al mismo tiempo: errores como “contexto de transacción en uso por otra sesión ”

Q3. Sí, se deriva a una transacción distribuida, por lo que alistar más de una conexión, incluso con la misma cadena de conexión, hace que se convierta en una transacción distribuida, lo que se puede confirmar buscando un GUID no nulo en Transaction.Current.TransactionInformation .DistributedIdentifier. * Actualización: leí en alguna parte que esto está solucionado en SQL Server 2008, por lo que MSDTC no se usa cuando se usa la misma cadena de conexión para ambas conexiones (siempre que ambas conexiones no estén abiertas al mismo tiempo). Esto le permite abrir una conexión y cerrarla varias veces en una transacción, lo que podría hacer un mejor uso del grupo de conexiones abriendo las conexiones lo más tarde posible y cerrándolas lo antes posible.

Q4. No. Una conexión abierta cuando no estaba activo el scope de la transacción, no se incluirá automáticamente en un ámbito de transacción recién creado.

Q5. No. A menos que abra una conexión en el scope de la transacción o enlista una conexión existente en el scope, básicamente NO HAY TRANSACCIÓN. Su conexión se debe enlistar automática o manualmente en el scope de la transacción para que sus comandos participen en la transacción.

Q6. Sí, los comandos en una conexión que no participa en una transacción se comprometen tal como se emiten, incluso aunque el código se haya ejecutado en un bloque de scope de transacción que se retrotrajo. Si la conexión no se alista en el scope de la transacción actual, no participa en la transacción, por lo que comprometer o revertir la transacción no tendrá ningún efecto en los comandos emitidos en una conexión no enlistada en el scope de la transacción … como este hombre descubrió . Es muy difícil de detectar a menos que entienda el proceso de alistamiento automático: se produce solo cuando se abre una conexión dentro de un scope de transacción activo.

Q7. Sí. Una conexión existente puede alistarse explícitamente en el scope de transacción actual llamando a EnlistTransaction (Transaction.Current). También puede enlistar una conexión en un hilo separado en la transacción usando un DependentTransaction, pero como antes, no estoy seguro de cómo dos conexiones involucradas en la misma transacción contra la misma base de datos pueden interactuar … y pueden ocurrir errores, y por supuesto, la segunda conexión alistada hace que la transacción escale a una transacción distribuida.

Q8. Un error puede ser arrojado. Si se utilizó TransactionScopeOption.Required, y la conexión ya se enlistó en una transacción de ámbito de transacción, entonces no hay ningún error; de hecho, no hay una transacción nueva creada para el scope, y el recuento de transacciones (@@ trancount) no aumenta. Sin embargo, si usa TransactionScopeOption.RequiereNuevo, recibirá un mensaje de error útil al intentar enlistar la conexión en la nueva transacción del scope de la transacción: “La conexión actualmente tiene transacción enlistada. Finalice la transacción actual y vuelva a intentarlo”. Y sí, si completa la transacción en la que se enganchó la conexión, puede alistar de forma segura la conexión en una nueva transacción. Actualización: si llamó previamente a BeginTransaction en la conexión, se produce un error ligeramente diferente cuando intenta enlistar en una nueva transacción de ámbito de transacción: “No se puede alistar en la transacción porque una transacción local está en progreso en la conexión. Finalice la transacción local y rever.” Por otro lado, puede llamar a BeginTransaction de manera segura en SqlConnection mientras se enlista en una transacción de scope de transacción, y eso realmente boostá @@ trancount en uno, a diferencia de usar la opción Requerido de un scope de transacción nested, que no lo hace incrementar. Curiosamente, si luego continúa creando otro scope de transacción nested con la opción Requerido, no obtendrá un error, porque nada cambia como resultado de tener una transacción de scope de transacción activa (recuerde que @@ trancount no se incrementa cuando una transacción la transacción del scope ya está activa y se usa la opción Requerido).

Q9. Sí. Los comandos participan en cualquier transacción en la que se enlista la conexión, independientemente de cuál sea el scope de la transacción activa en el código C #.

Buen trabajo Triynko, todas tus respuestas parecen bastante precisas y completas para mí. Algunas otras cosas que me gustaría señalar:

(1) alistamiento manual

En su código anterior, usted (correctamente) muestra el alistamiento manual de esta manera:

 using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); using (TransactionScope ts = new TransactionScope()) { conn.EnlistTransaction(Transaction.Current); } } 

Sin embargo, también es posible hacerlo así, usando Enlist = false en la cadena de conexión.

 string connStr = "...; Enlist = false"; using (TransactionScope ts = new TransactionScope()) { using (SqlConnection conn1 = new SqlConnection(connStr)) { conn1.Open(); conn1.EnlistTransaction(Transaction.Current); } using (SqlConnection conn2 = new SqlConnection(connStr)) { conn2.Open(); conn2.EnlistTransaction(Transaction.Current); } } 

Hay otra cosa para notar aquí. Cuando se abre conn2, el código del grupo de conexiones no sabe que desea enlistarlo más tarde en la misma transacción que conn1, lo que significa que a conn2 se le da una conexión interna diferente que conn1. Luego, cuando conn2 se alista, ahora hay 2 conexiones enlistadas, por lo que la transacción debe promocionarse a MSDTC. Esta promoción solo se puede evitar mediante el alistamiento automático.

(2) Antes de .Net 4.0, recomiendo configurar “Enlace de transacción = Desvinculación explícita” en la cadena de conexión . Este problema está solucionado en .Net 4.0, lo que hace que Explicit Unbind sea totalmente innecesario.

(3) CommittableTransaction su propia Transaction.Current CommittableTransaction y configurando Transaction.Current a eso es esencialmente lo mismo que lo que hace TransactionScope . Esto rara vez es realmente útil, solo para tu información.

(4) Transaction.Current es thread-static. Esto significa que Transaction.Current solo está configurado en el hilo que creó TransactionScope . Por lo tanto, no es posible ejecutar varios hilos ejecutando el mismo TransactionScope (posiblemente utilizando la Task ).

Otra situación extraña que hemos visto es que si construyes un EntityConnectionStringBuilder se EntityConnectionStringBuilder con TransactionScope.Current y (creemos) alistar en la transacción. Hemos observado esto en el depurador, donde current.TransactionInformation.internalTransaction TransactionScope.Current muestra enlistmentCount == 1 antes de la construcción, y enlistmentCount == 2 después.

Para evitar esto, construirlo dentro

using (new TransactionScope(TransactionScopeOption.Suppress))

y posiblemente fuera del scope de su operación (lo estábamos construyendo cada vez que necesitábamos una conexión).