rebus-org / Rebus.SqlServer

:bus: Microsoft SQL Server transport and persistence for Rebus
https://mookid.dk/category/rebus
Other
44 stars 45 forks source link

Outbox implementation with TransactionScope #99

Open JornWildt opened 1 year ago

JornWildt commented 1 year ago

The current Outbox implementation requires both an SqlConnection as well as an SqlTransaction as shown on https://github.com/rebus-org/Rebus/wiki/Outbox :

using var connection = GetSqlConnection();
using var transaction = connection.BeginTransaction();
using var scope = new RebusTransactionScope();

// and then enable the outbox for that scope
scope.UseOutbox(connection, transaction);

Unfortunately I work with a framework that uses TransactionScope - and I have found no way to get the SqlTransaction out of that.

Is there any way to get SqlTransaction out of SqlConnection (I haven't found one)?

Would it be possible to expand the Outbox implementation to work with TransactionScope in addition to SqlTransaction?

mookid8000 commented 1 year ago

Hmm curious as you made me I went through QuickWatch reflection on SqlConnection, and as you can see there's a SqlTransaction hiding in there, which can be retrieved through some private properties: image

I then coded this little example to see if I could actually get it to work. The central bits look somewhat like this:

static SqlTransaction GetSqlTransactionViaReflection(SqlConnection connection)
{
    const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

    var innerConnectionField = connection.GetType().GetProperty("InnerConnection", flags);
    var innerConnection = innerConnectionField?.GetValue(connection);

    var availableInnerTransactionField = innerConnection?.GetType().GetProperty("AvailableInternalTransaction", flags);
    var availableInnerTransaction = availableInnerTransactionField?.GetValue(innerConnection);

    var parentTransactionField = availableInnerTransaction?.GetType().GetProperty("Parent", flags);
    var parentTransaction = parentTransactionField?.GetValue(availableInnerTransaction) as SqlTransaction;

    return parentTransaction;
}

It succeeds in retrieving the SqlTransaction when it's an ordinary, manually started transaction, but it fails when the transaction is an ambient one started by the TransactionScope.

I have not been able to figure out a way to retrieve the transaction in any other way.

Maybe it would be a nice addition to the API to enable doing something like this:

// somewhere, possibly far out in the call hierarchy
using var txScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

// further down:
using var scope = new RebusTransactionScope();

scope.UseOutboxWithAmbientTransaction();

// whee!

I don't think it would be too hard to enable this, but it's hard to tell if there's a surprise or two lurking in there.

JornWildt commented 1 year ago

Thanks for your work! scope.UseOutboxWithAmbientTransaction(); seems to fit my exact use case where my framework uses TransactionScopeall over the place. Should you ever find time to add it, then please ping me here :-)

BTW it is Cofoundry: https://www.cofoundry.org/docs/framework/data-access/transactions