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?
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.
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.