Outbox, SynchronizedStorageSession, and UnitOfWork confusion

sql-persistence

(Mike Sigsworth) #1

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!


Transaction Scopes when enabling Outbox
(Ramon Smits) #2

This is pretty common for customer that use the repository pattern. There is a workaround for this:

In NServiceBus 5 the NHibernateStorageContext storage context was registered in the container. In NServiceBus 6 this does not work anymore because the context is not registered in the container and cannot be injected via the constructor any more. It can only be passed as an argument meaning you have to pass the context as argument into the IOrderRepository.Add(Order) which is - obviously - not what you want if you use the repository pattern. This sample extends NServiceBus with behavior very similar to NHibernateStorageContext which was available prior to NServiceBus 6.

You can use that as inspiration on how you can use dependency injection to pass on the IDbConnection for your code that uses Dapper.

Let me know if that helps!

– Ramon


(Mike Sigsworth) #3

Thank you for that! I spent the morning tinkering with it and experimenting with various scenarios. I forked the code and created a version that supports Autofac and SQL Persistence.


(Ramon Smits) #4

Glad it helped you resolve your issue. I inspected your fork, I just wondered why you used the registration API of Autofac and not the NServiceBus API for registering components. I personally try to avoid non NServiceBus APIs for configuring behavior in the NServiceBus pipeline if possible.


(Mike Sigsworth) #5

To be honest, I had no idea NServiceBus even had that registration API. Since we are using Autofac within our services already, and for more than just NServiceBus, it makes sense to keep the DI configuration in one place. I wanted to ensure that the solution you provided would still work within our existing services.

Thanks again for pointing us in the right direction. It was a huge help!

Oh, and also… to answer one of my own questions. I ran a test yesterday to once and for all understand when MS DTC gets involved in a TransactionScope. Short answer is, as soon as you open a second DbConnection, your “lightweight transaction” is promoted to a distributed transaction and thus MS DTC must get involved. If MS DTC is disable, then the 2nd connection fails. Even if it’s using the exact same connection string to start the 2nd connection, MS DTC must be enabled. This was a bit of a surprise. Somehow I thought that if you connected to the exact same DB on the same server, it wouldn’t require MS DTC. But that was wrong.

Anyways. Thanks a ton!


(Mike Sigsworth) #6

@ramonsmits How does this work in a Web API or MVC situation where you just have the IEndpointInstance? Any suggestions?

Our repository classes are used by both our handlers and our controllers. So the behavior works based on the IMessageHandlerContext for a handler, correct? So… how do we manage the connection/transaction for Web API?

Thanks!