Warning… long post is long.
We have recently completed migrating all of our services to NSB6, SqlPersistence, and Outbox. Ultimately we have our sights set on disabling MS DTC. But we have encountered an issue that we did not foresee at the start of all this. Our data access all uses Dapper. So when we need to access data we create a new SqlConnection
, do the query, and then dispose the connection (it’s all in a using statement). This means, we are not using the SynchronizedStorageSession
.
As you can imagine, once we enabled Outbox, we started seeing partial updates. To fix this we enabled NSB’s UnitOfWork
, i.e WrapHandlersInATransactionScope()
. However, I have some concerns about using this if we really want to get away from MS DTC.
I think my understanding of how TransactionScopes get promoted to distributed transactions is murky. And the documentation, though terribly fun to read \s, has not helped. For example, one thing that is not clear is, if we establish two unique SqlConnections within a TransactionScope
, but they use the same connection string, does that require DTC?
We decided to try something different, and get away from the UnitOfWork
. But we are essentially doing our own UoW pattern now. Essentially, we’ve now fallen back to try and retrofit our data access code to use the SynchronizedStorageSession
. But, as you can imagine, this is a tremendous change to our code base. The fundamental issue we are encountering right now is, our “persister” and “provider” classes are injected into our handlers (via Autofac), and we cannot inject the SynchronizedStorageSession
, because it is only available via the Handler method signature.
I am mulling over a design where we would create a new class called DbSession
. We could inject this into our persister/provider classes. The DbSession
would simply be a distribution mechanism for the SynchronizedStorageSession
Connection and Transaction. Perhaps some code will help illustrate the idea:
public interface IDbSession
{
IDbConnection Connection { get; }
IDbTransaction Transaction { get; }
void Initialize(IDbConnection conn, IDbTransaction tx);
}
public class DbSession : IDbSession
{
public IDbConnection Connection { get; }
public IDbTransaction Transaction { get; }
public void Initialize(IDbConnection conn, IDbTransaction tx)
{
Connection = conn;
Transaction = tx;
}
}
public class BarProvider: IBarProvider
{
private IDbSession _dbSession;
public BarProvider(IDbSession dbSession)
{
_dbSession = dbSession;
}
public async Task<Bar> Get(Guid barId)
{
return (await _dbSession.QueryAsync(
"SELECT * FROM dbo.Bar WHERE barId = @barId",
new { barId },
_dbSession.Transaction)).SingleOrDefault();
}
}
public class FooHandler: IHandleMessages<Foo>
{
private IDbSession _dbSession;
private IBarProvider _barProvider;
public FooHandler(IDbSession dbSession, IBarProvider barProvider)
{
_dbSession = dbSession;
_barProvider = barProvider;
}
public async Task Handle(Foo foo, IMessageHandlerContext context)
{
// Get the SQL Persistence Session
var session = context.SynchronizedStorageSession.SqlPersistenceSession();
// Initialize the dbSession that is injected to the other dependencies in this lifetime scope
_dbSession.Initialize(session.Connection, session.Transaction);
// Invoke the provider, which will use the just initialized connection and transaction from the dbSession
var bar = await _barProvider.Get(foo.BarId);
// ...
}
}
A few things with this code. We are not seeing the Transaction get cleaned up by NSB. I was under the impression that the connection is “fully managed”. So what are our responsibilities here with connection management exactly?
Also, having to initialize the dbSession
like this in every handler is error prone. Can this be done in a behavior? Do behaviors get invoked for every handler invocation, and have access to the context?
And finally, to make this work we need to ensure that the dbSession
AutoFac lifetime is constrained to a single handler. Any suggestions on how to achieve this would also be welcomed.
And finally, finally… Is this even worth it? Could/Should we just use the NSB UnitOfWork
with its TransactionScope
?
Thank you!