The SQL Server transport supports the following transport transaction modes:
- Transaction scope (distributed transaction)
- Transport transaction - Send atomic with receive
- Transport transaction - receive only
- Unreliable (transactions disabled)
TransactionScope
mode is particularly useful as it enables exactly once
message processing with distributed transactions. However, when transport, persistence, and business data are all stored in a single SQL Server catalog, it is possible to achieve exactly-once
message delivery without distributed transactions. For more details, refer to the SQL Server native integration sample.
Exactly once
message processing without distributed transactions can be achieved with any transport using the Outbox feature. It requires business and persistence data to share the storage mechanism but does not put any requirements on transport data storage.Transaction scope
Distributed transactions are not supported in .NET Core
Although the .NET Core SQL Server driver supports enlisting in ambient transactions, it does not support participating in distributed transactions so any attempt to use more than one transactional resource within the same ambient transaction will cause an error.
Prior to Version 2.1, the .NET Core SQL Server driver did not support enlisting in ambient transactions and any attempt to do so resulted in the following exception:
NServiceBus.Transport.SQLServer.MessagePump|Sql receive operation failed
System.NotSupportedException: Enlisting in Ambient transactions is not supported.
To resolve this, choose one of the other transaction modes. Support for TransactionScope
is expected to be added in future releases of .NET Core.
It is still possible to use TransactionScope
in .NET Core but promoting local transactions to distributed transactions is no longer supported. Multiple connections can be used in the same transaction scope as long as they share the same connection string and connections are not open at the same time. If TransactionScope
is used within .NET Core, NServiceBus will log a warning to make sure that this is an explicit decision.
In this mode, the ambient transaction is started before receiving the message. The transaction encompasses all stages of processing including user data access and saga data access.
If either the configured NServiceBus persistence mechanism or the user data access also supports transactions via TransactionScope
, the ambient transaction could be promoted to a distributed transaction.
See also a sample covering this mode of operation using either SQL Persistence or NHibernate Persistence.
Native transactions
In this mode, the message is received inside a native ADO.NET transaction
There are two available options within native transaction level:
- ReceiveOnly - An input message is received using native transaction. The transaction is committed only when message processing succeeds.
- SendsAtomicWithReceive - This mode is similar to the
ReceiveOnly
, but transaction is shared with sending operations. That means the message receive operation and any send or publish operations are committed atomically.
Unreliable (transactions disabled)
In this mode, when a message is received it is immediately removed from the input queue. If processing fails the message is lost because the operation cannot be rolled back. Any other operation that is performed when processing the message is executed without a transaction and cannot be rolled back. This can lead to undesired side effects when message processing fails part way through.
User provided transactions
When sending messages outside of a handler context it is possible to provide custom SqlConnection
and SqlTransaction
instances that will be used to perform transport operations. This enables sending two or more message in a single, atomic transaction or share the same transaction between transport and relational data store.
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
var sqlCommand = new SqlCommand(commandText, connection, transaction);
//Execute SQL statement
sqlCommand.ExecuteNonQuery();
//Send a message
var sendOptions = new SendOptions();
sendOptions.UseCustomSqlTransaction(transaction);
await session.Send(new Message(), sendOptions);
transaction.Commit();
}
}