How to check whether DbContext has transaction?

2019-04-07 02:53发布

问题:

Background: I have WCF service with SimpleInjector as IoC which creates instance of DbContext per WCF request.

Backend itself is CQRS. CommandHandlers have a lot of decorators (validation, authorization, logging, some common rules for different handler groups etc) and one of them is Transaction Decorator:

public class TransactionCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand> 
    where TCommand : ICommand
{
    private readonly ICommandHandler<TCommand> _handler;
    private readonly IMyDbContext _context;
    private readonly IPrincipal _principal;

    public TransactionCommandHandlerDecorator(ICommandHandler<TCommand> handler,
        IMyDbContext context, IPrincipal principal)
    {
        _handler = handler;
        _context = context;
        _principal = principal;
    }

    void ICommandHandler<TCommand>.Handle(TCommand command)
    {
        using (var transaction = _context.Database.BeginTransaction())
        {
            try
            {
                var user = _context.User.Single(x => x.LoginName == _principal.Identity.Name);
                _handler.Handle(command);
                _context.SaveChangesWithinExplicitTransaction(user);
                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();
                throw;
            }
        }
    }
}

Problem occurs when any command tries to chain execute another command within the same WCF request. I got an expected exception at this line:

using (var transaction = _context.Database.BeginTransaction())

because my DbContext instance already has a transaction.

Is there any way to check current transaction existence?

回答1:

Instead of using the transaction from the DbContext of Entity Framework you could or maybe should use the TransactionScope class which creates an ambient transaction scope and manages transactions of all connections made to the (SQL) database under the covers.

It even would put a direct SqlCommand in the same transaction if you would use the exact (case-sensitive) connectionstring for the SqlCommand. Messages writen to the MessageQueue are also encapsulated in the same transaction

It even could manage connections to different databases at the same time. It uses the DTC windows service for this. Beware that this is a pain to configure if needed. Normally, with a single DB connection (or multiple connections to the same DB) you won't need the DTC.

The TransactionScopeCommandHandlerDecorator implementation is trivial:

public class TransactionScopeCommandHandlerDecorator<TCommand> 
        : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decoratee;

    public TransactionScopeCommandHandlerDecorator(ICommandHandler<TCommand> decoratee)
    {
        this.decoratee = decoratee;
    }

    public void Handle(TCommand command)
    {
        using (var scope = new TransactionScope())
        {
            this.decoratee.Handle(command);

            scope.Complete();
        }
    }
}

But: As qujck already mentioned in the comments, you are missing the concept of ICommandHandler as an atomic operation. One commandhandler should never reference another commandhandler. Not only is this bad for transactions, but also consider this:

Imagine the application grows and you would refactor some of your commandhandlers to a background thread, which will run in some windows service. In this windows service a PerWcfOperation lifestyle is not available. You would need a LifeTimeScope lifestyle for you commandhandlers now. Because your design allows it, which is great by the way!, you would typicaly wrap your commandhandlers in a LifetimeScopeCommandHandler decorator to start the LifetimeScope. In your current design where a single commandhandler references other commandhandlers you will run into a problem, because every commandhandler will be created in its own scope a thus gets an other DbContext injected than the other commandhandlers!

So you need to do some redesign and make your commandhandlers holistic abstractions and create a lower level abstraction for doing the DbContext operations.



回答2:

I think you're looking for the CurrentTransaction property of the DbContext:

var transaction = db.Database.CurrentTransaction;

Then you can do a check like this:

using(var transaction = db.Database.CurrentTransaction ?? db.Database.BeginTransaction())
{
   ...
}

However I'm not sure how you can know when to commit the transaction if it's being used by concurrent methods.