8

I'd like to write a simple unit of work class that would behave like this:

using (var unitOfWork = new UnitOfWork())
{
   // Call the data access module and don't worry about transactions.
   // Let the Unit of Work open a session, begin a transaction and then commit it.
}

This is what I have so far (any comments will be welcome if you think my design is wrong):

class UnitOfWork : IDisposable
{
   ISession _session;
   ITransation _transaction;
   .
   .
   .
   void Dispose()
   {
      _transaction.Commit();
      _session.Dispose();
   }
}

What I would like to do is to roll back the transaction in case the data acess code threw some exception. So the Dispose() method would look something like:

   void Dispose()
   {
      if (Dispose was called because an exception was thrown) 
      {
         _transaction.Commit();
      }
      else
      {
         _transaction.RollBack();
      }
      _session.Dispose();
   }

Does it make sense? And if so, how can it be done?

Ilya Kogan
  • 21,995
  • 15
  • 85
  • 141

5 Answers5

6

The "Dispose()" should have nothing to do with transaction commit or rollback. You should dispose you transaction in your Dispose() method. Changing the semantics of your Dispose() method will only add to confusion in the long run for you and anyone else using your class.

The Transaction's Commit() and RollBack() methods have nothing whatsoever to do with a Dispose() method since there is no correlation between these two method and Dispose(), since you have to dispose a transaction no matter what the final outcome is.

This is the correct pattern to use as regards connections and transactions. Notice that Roolback(0 relates to an exception (and not dispose)

connection.Open();
var trasnaction = null;
try
{
  transaction = connection.BeginTransaction(); 
  ///Do Some work
  transaction.Commit();
}
catch
{
  transaction.Rollback();
}
finally
{
  if (transaction != null)
    transaction.Dispose();
  connection.Close();
}

So mimic this pattern in your UnitOfWork, with methods for Commit(), Roolback() and Dispose().

Shiv Kumar
  • 9,599
  • 2
  • 36
  • 38
  • This is exactly what I want to avoid. I don't expect the user to remember to call Commit() in the end of each block. I want the Commit() to happen by itself. But never mind, I think I've come to a good solution. I'll post it next week. – Ilya Kogan Mar 10 '11 at 17:51
  • @llya, not sure why the "user" has to call or do anything besides initiate the process. The code I've shown is part of a method. The user should only have to initiate the process (call the method). – Shiv Kumar Mar 10 '11 at 18:57
  • Rollback in a Dispose is entirely proper and idiomatic. Whenever possible, a class should be designed so that any object which isn't going to be used again may be safely abandoned after calling IDisposable.Dispose, without having to do anything else. If a database requires that any transaction that isn't ultimately going to be Commit'ed should be Rollback'ed, it is much cleaner for the connection object to be encapsulated in such a way to allow its responsibility to be handled via IDisposable.Dispose than to require some non-standard mechanism to ensure it's left in a reasonable state. – supercat Mar 11 '11 at 21:26
  • Could you explain why transaction is disposed in the finally block although transaction.RollBack() has been called in the catch block? – Efe Zaladin Aug 17 '21 at 21:00
4

A bit late to the game here, but check this post from Ayende for a (slightly mad) solution:

In the Dispose method, you just need to figure out whether or not it got there 'cleanly' - i.e. figure out if an unhandled exception exists before committing the transaction:

public class ExceptionDetector : IDisposable
{
    public void Dispose()
    {
        if (Marshal.GetExceptionCode()==0)
            Console.WriteLine("Completed Successfully!");
        else
            Console.WriteLine("Exception!");
    }
}
Squiggle
  • 2,427
  • 1
  • 18
  • 22
  • 2
    What are the exact semantics of that? What happens if one `Dispose` method chains to another method within a `try`/`finally` block? – supercat May 02 '13 at 19:24
  • `if (Marshal.GetExceptionCode() == 0) _repository.CommitUnitOfWork();` – Squiggle May 13 '13 at 13:33
  • If one `Dispose` method within a `finally` block chains to another which is within a try/finally block, will the `Marshal.GetExceptionCode` report the status of the innermost block or the outer block, or what? – supercat May 13 '13 at 14:57
  • 1
    This is obsolete for .NET Core https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getexceptioncode?view=netcore-1.0 – Isaac Llopis Jul 31 '20 at 08:28
3

The point of Dispose is that it is always run. And by using this commit-rollback idiom you don't need to know the difference.

using (var unitOfWork = new UnitOfWork())
{
    // use unitOfWork here - No need to worry about transactions for this code.
    unitOfWork.Commit();
}

Here we see that either an exception is thrown or the unitOfWork is committed. We can then have a bool in UnitOfWork keeping track whether the Commit was executed or not. Then the Dispose can Rollback not commited. This way the unit-of-work is always either rollbacked or commited.

I would in any case avoid having the Commit inside the Dispose. For starters a ITransaction.Commit method typically may throw exceptions on errors - This is perfectly normal. However, the Dispose method is not supposed to throw exceptions. See this link and search on Stackoverflow for more indepth information on why.

I'm thinking something like this in big strokes

class UnitOfWork : IDisposable
{
   ISession _session;
   ITransation _transaction;
   bool _commitTried;

   // stuff goes here

   void Commit()
   {
      _commitTried = true;
      _transaction.Commit();
   }

   void Dispose()
   {
      if (!_commitTried) _transaction.Rollback();
      _transaction.Dispose();
      _session.Dispose();
   }
}

The problem with forgetting to call Commit altogether I would say is not that big since unless commited, the client code will not work since the transaction is Rollback'ed and changes are not applied which will be discovered when exercising the code inside a fixture or manually.

I actually tried to deal with this in one project by using a syntax with lambdas like so

_repository.InTransactionDo(ThisMethodIsRunInsideATransaction);

This way clients did not need to worry about Commiting anything. I actually ended up regretting this since it complicated things too much and wished I hade gone with the above approach.

vidstige
  • 12,492
  • 9
  • 66
  • 110
  • This is exactly what I want to avoid. I don't expect the user to remember to call Commit() in the end of each block. I want the Commit() to happen by itself. But never mind, I think I've come to a good solution. I'll post it next week. – Ilya Kogan Mar 10 '11 at 17:50
  • Alright, I'm going to edit my post a bit to direct that. Looking forward to your post then! – vidstige Mar 11 '11 at 07:18
  • @Ilya Kogan Edited my post a bit to adress the problem with users forgetting to call Commit(). – vidstige Mar 11 '11 at 07:37
  • @vidstige This is exactly what I'm intending to do, great minds think alike :) – Ilya Kogan Mar 11 '11 at 13:39
  • @Ilya Kogan haha cool. or simple minds perhaps. ;) Anyway, as I wrote in the end I actually regreted that decision since it complicated things on the user side. Good luck anyway! – vidstige Mar 11 '11 at 15:10
  • I would say that rollbacks should be designed to succeed, but a rollback which fails should let loose an exception. Note that failed commits which turn into rollbacks are far more common and far less alarming that failed rollbacks. In the former case, the state of the database is to some extent known. In the latter, it isn't. – supercat Mar 11 '11 at 21:15
  • @supercat I'm using crashreporting on an appilcation with _a lot_ of users and never seen rollback fail. I guess it depends on what database and transaction settings you are using. Also I never rollback a transaction other than in the event of an exception. – vidstige Mar 12 '11 at 07:24
  • 1
    @vidstige: The Consensus Problem means that when dealing with a remote system there will always be a possibility of a commit failing in such fashion that the code attempting to perform it won't know whether it succeeded or not unless or until it can query the remote system. If the factor that caused the commit failure makes the remote system unavailable (e.g. a break in communications) that might not be possible for awhile. Rollbacks that can't be proven to have occurred can easily occur if communications are unreliable. Genuinely failed rollbacks are rare, but should be VERY alarming. – supercat May 02 '13 at 19:19
  • @vidstige: Another thing to consider is that data access layers may not support rollbacks in all situations, and `using` blocks may guard resources that don't have no concept of a rollback. The `using` statement works nicely with lock tokens (creating an object acquires a lock, and `Dispose` releases the lock), but it would be enhanced substantially if one could say `using (var token = myLock.EnterDangerBlock())`, and have the `Dispose` method *invalidate* `myLock` if the `using` block exits via exception. Note that in many cases, the proper behavior if code exits a lock unexpectedly... – supercat May 06 '13 at 16:45
  • @vidstige: ...should be neither holding the lock forever, nor releasing the lock and letting other code reacquire it; the right behavior in many cases should be to invalidate the lock so all present and future attempts to acquire it will fail with an immediate exception. Such behavior would guard against corruption just as effectively as would leaving the lock held forever, but would make it easier for code that would be able to recover from that situation to do so. – supercat May 06 '13 at 16:49
  • @supercat I don't see how locks are involved here? Or is it just an analogy? Seems so in the beginning of your rant, less so at the end ^^ Would this rant be in favor or agaist my posted solution? – vidstige May 07 '13 at 08:07
  • @vidstige: The pattern "Get using-guarded token to do an atomic unit of work; operate on token; commit it; if not committed before `Dispose`, try to roll back" is a good one if the work can in fact be cleanly rolled back. If one is using databases which are designed to ensure that any operation can be successfully rolled back, the risk of being caught in an awkward situation may be small, but the general pattern is often used with other types of data structures that allow for atomic operations but may not allow for nice rollbacks. A lock-token factory is a simple example of such. – supercat May 07 '13 at 15:15
  • @vidstige: I think my main points are: (1) some types of data structures might allow for rollbacks in some cases but not others; (2) even if one thinks that transactions which involve a commit should explicitly specify the commit rather than having it be implicit in the fact that a `using` block exited without exception, if the unit-of-work isn't going to support rollback (e.g. it's a lock-token factory), having to "commit" to let the "using" block cleanup know it's being exited normally seems awkward. BTW, another advantage of having a `Dispose` block know whether there's an exception... – supercat May 07 '13 at 15:23
  • @vidstige: ...is that if one takes the view that main-line code should either or explicitly roll back all transactions, then exiting a transaction scope, *other than via exception*, without doing either should trigger an exception at the point of the infraction. It would be rather annoying, however, if an exception within a transaction scope caused its cleanup block to throw an `FailureToCommitOrRollbackException` (overwriting the exception that caused its departure). – supercat May 07 '13 at 15:27
2

You need to UnitOfWork.Commit() at the end of the using block. Inside UnitOfWork you have a committed flag that you check in UnitOfWork.Dispose. If the flag is false there, then you UnitOfWork.Rollback().

Johann Gerell
  • 24,991
  • 10
  • 72
  • 122
1

After all, I implemented a method through which all operations should be performed:

class UnitOfWork : IDisposable
{
   ...
   public void DoInTransaction(Action<ISession> method)
   {
       Open session, begin transaction, call method, and then commit. Roll back if there was an exception.
   }
}
Ilya Kogan
  • 21,995
  • 15
  • 85
  • 141