I'm just now sitting with the same problem and to me there seems to be two solutions:
- Don't solve the problem.
- Create abstractions for the existing classes that follows the same pattern but are mockable/stubable.
EDIT:
I've created a CodePlex-project for this now: http://legendtransactions.codeplex.com/
End of edit
I'm leaning towards creating a set of interfaces for working with transactions and a default implementation that delegates to the System.Transaction-implementations, something like:
public interface ITransactionManager
{
ITransaction CurrentTransaction { get; }
ITransactionScope CreateScope(TransactionScopeOption options);
}
public interface ITransactionScope
: IDisposable
{
void Complete();
}
public interface ITransaction
{
void EnlistVolatile(IEnlistmentNotification enlistmentNotification);
}
public interface IEnlistment
{
void Done();
}
public interface IPreparingEnlistment
{
void Prepared();
}
public interface IEnlistable // The same as IEnlistmentNotification but it has
// to be redefined since the Enlistment-class
// has no public constructor so it's not mockable.
{
void Commit(IEnlistment enlistment);
void Rollback(IEnlistment enlistment);
void Prepare(IPreparingEnlistment enlistment);
void InDoubt(IEnlistment enlistment);
}
This seems like a lot of work but on the other hand it's reusable and it makes it all very easily testable.
Note that this is not the complete definition of the interfaces just enough to give you the big picture.
EDIT:
I just did some quick and dirty implementation as a proof of concept, I think this is the direction I will take, here's what I've come up with so far. I'm thinking that maybe I should create a CodePlex-project for this so the problem can be solved once and for all. This is not the first time I've run into this.
public interface ITransactionManager
{
ITransaction CurrentTransaction { get; }
ITransactionScope CreateScope(TransactionScopeOption options);
}
public class TransactionManager
: ITransactionManager
{
#region ITransactionManager Members
public ITransaction CurrentTransaction
{
get { return new DefaultTransaction(Transaction.Current); }
}
public ITransactionScope CreateScope(TransactionScopeOption options)
{
return new DefaultTransactionScope(new TransactionScope());
}
#endregion
}
public interface ITransactionScope
: IDisposable
{
void Complete();
}
public class DefaultTransactionScope
: ITransactionScope
{
private TransactionScope scope;
public DefaultTransactionScope(TransactionScope scope)
{
this.scope = scope;
}
#region ITransactionScope Members
public void Complete()
{
this.scope.Complete();
}
#endregion
#region IDisposable Members
public void Dispose()
{
this.scope.Dispose();
}
#endregion
}
public interface ITransaction
{
void EnlistVolatile(Enlistable enlistmentNotification, EnlistmentOptions enlistmentOptions);
}
public class DefaultTransaction
: ITransaction
{
private Transaction transaction;
public DefaultTransaction(Transaction transaction)
{
this.transaction = transaction;
}
public void EnlistVolatile(Enlistable enlistmentNotification, EnlistmentOptions enlistmentOptions)
{
this.transaction.EnlistVolatile(enlistmentNotification, enlistmentOptions);
}
}
public interface IEnlistment
{
void Done();
}
public interface IPreparingEnlistment
{
void Prepared();
}
public abstract class Enlistable
: IEnlistmentNotification
{
public abstract void Commit(IEnlistment enlistment);
public abstract void Rollback(IEnlistment enlistment);
public abstract void Prepare(IPreparingEnlistment enlistment);
public abstract void InDoubt(IEnlistment enlistment);
void IEnlistmentNotification.Commit(Enlistment enlistment)
{
this.Commit(new DefaultEnlistment(enlistment));
}
void IEnlistmentNotification.InDoubt(Enlistment enlistment)
{
this.InDoubt(new DefaultEnlistment(enlistment));
}
void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
{
this.Prepare(new DefaultPreparingEnlistment(preparingEnlistment));
}
void IEnlistmentNotification.Rollback(Enlistment enlistment)
{
this.Rollback(new DefaultEnlistment(enlistment));
}
private class DefaultEnlistment
: IEnlistment
{
private Enlistment enlistment;
public DefaultEnlistment(Enlistment enlistment)
{
this.enlistment = enlistment;
}
public void Done()
{
this.enlistment.Done();
}
}
private class DefaultPreparingEnlistment
: DefaultEnlistment, IPreparingEnlistment
{
private PreparingEnlistment enlistment;
public DefaultPreparingEnlistment(PreparingEnlistment enlistment)
: base(enlistment)
{
this.enlistment = enlistment;
}
public void Prepared()
{
this.enlistment.Prepared();
}
}
}
Here's an example of a class that depends on the ITransactionManager to handle it's transactional work:
public class Foo
{
private ITransactionManager transactionManager;
public Foo(ITransactionManager transactionManager)
{
this.transactionManager = transactionManager;
}
public void DoSomethingTransactional()
{
var command = new TransactionalCommand();
using (var scope = this.transactionManager.CreateScope(TransactionScopeOption.Required))
{
this.transactionManager.CurrentTransaction.EnlistVolatile(command, EnlistmentOptions.None);
command.Execute();
scope.Complete();
}
}
private class TransactionalCommand
: Enlistable
{
public void Execute()
{
// Do some work here...
}
public override void Commit(IEnlistment enlistment)
{
enlistment.Done();
}
public override void Rollback(IEnlistment enlistment)
{
// Do rollback work...
enlistment.Done();
}
public override void Prepare(IPreparingEnlistment enlistment)
{
enlistment.Prepared();
}
public override void InDoubt(IEnlistment enlistment)
{
enlistment.Done();
}
}
}