En un servicio web, estoy consultando una base de datos de SQL Server 2016. Usando .NET TransactionScope de la siguiente manera para mantener la gestión de transacciones en mi capa de servicio pero las consultas/comandos de datos en mi código de capa de datos (clases de "almacenamiento"), tenemos algunos lugares que siguen este patrón:
using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { bool needsInsert = await store1.Exists(request.id); if (needsInsert) mainRowsUpdatedCount = await store2.Insert(request); transaction.Complete(); }Cada uno de esos métodos de "almacenamiento" sigue este patrón (usando Dapper, aunque sospecho que no importa):
const string query = @"SELECT ..."; // or INSERT or MERGE as the case may be using IDbConnection connection = new SqlConnection(ConnectionString.Value); return await connection.QueryAsync<T>(query, new { ... }); // or connection.ExecuteAsync as the case may beEsto funciona muy bien en la mayoría de las llamadas, pero a veces recibo lo siguiente (aunque muy raramente):
System.PlatformNotSupportedException: esta plataforma no admite transacciones distribuidas.
Entonces, ¿podría ser que, en el ejemplo anterior, store1.Exists se ejecuta, obtiene una conexión, lo inscribe en la transacción, ejecuta su consulta, se cierra y, a veces, antes de que se pueda ejecutar store2.Insert, algún otro subproceso no relacionado obtiene la misma conexión de el grupo de conexiones que ya tiene una transacción abierta, intenta ejecutar una consulta y, por lo tanto, lanza una PlatformNotSupportedException, ya que .NET Core (o .NET 5+) no admite transacciones distribuidas?
Si es así, ¿cómo puedo superar esto sin pasar mis conexiones?
Si no, ¿qué más podría estar causando esta excepción?
Encontré el mismo problema (también muy raramente) y, finalmente, llegué a la conclusión de que lo más probable es que se trate de un error en las bibliotecas base de .NET Cores (ya sea SqlClient o Transaction).
Según tengo entendido, hay una diferencia entre una transacción .NET y una transacción SQL local. Cuando crea un TransactionScope , inicia efectivamente una nueva transacción .NET a la que siempre se puede acceder a través de Transaction.Current . Llamar a .Open() en una SqlConnection recién creada detecta internamente esta transacción .NET ambiental y consulta el conjunto de conexiones internas, donde se almacenan las conexiones SQL reales , para cualquier conexión existente adecuada (es decir, con la misma cadena de conexión) que ya está asociado con esa transacción ambiental .NET. Si hay uno disponible (es decir, libre/inactivo), lo usa directamente sin escalar a una transacción distribuida.
Por lo tanto, si todo su código dentro de un TransactionScope garantiza tener siempre como máximo una SqlConnection abierta en un momento dado , se debe garantizar que la misma conexión SQL real y, por lo tanto, la transacción SQL local se reutilice bajo el capó y no se intente escalar a una transacción distribuida debe hacerse.
En su ejemplo, usa TransactionScopeAsyncFlowOption.Enabled para garantizar que la transacción ambiental se mantenga correctamente a través del flujo de código asíncrono (es decir, se permite usar await dentro del alcance). Mientras no se salga de este flujo, todo debería estar bien.
Incluso si hay otro TransactionScope (y, por lo tanto, otra transacción .NET) creada en algún lugar de la aplicación, con TransactionScopeAsyncFlowOption.Suppress , fuera de su alcance, que no se eliminó correctamente, y su hilo asociado resulta ser el que continúa su código asincrónico fluya dentro de su alcance, entonces no debería haber ninguna combinación, ya que su próxima SqlConnection intenta ser parte de la transacción .NET filtrada o su transacción .NET. No esperaría una escalada de cualquier manera. Probablemente esta circunstancia arroja un tipo diferente de excepción anteriormente (no probada).
Considerándolo todo, sigo creyendo que se trata de un error enterrado en el código de espagueti de transacciones de .NET. ;) Mi problema es que solo encuentro la excepción en casos raros en producción, simplemente no puedo reproducirla usando una prueba unitaria o algo así.