6

I wrote a UnitOfWork implementation that does not expose a public Commit() method. Instead the UnitOfWork implements IDisposable and the Commit is executed in the Dispose() method. I don't see any immediate problems with this, but it seems unorthodox so I want to know if you guys can point out some major reason not to do this that I am overlooking.

Here is some sample code:

public class DataService
{
    public DataService()
    {
        _db = new MyDataContext();
        _exceptionHandler = new SqlExceptionHandler();
    }
    private readonly MyDataContext _db;
    private readonly SqlExceptionHandler _exceptionHandler;
    public void Add(Product product, Cart cart)
    {
        using(UnitOfWork unitOfWork = new UnitOfWork(_db, ex=>_exceptionHandler.Handle(ex)))
        {
            unitOfWork.Create<CartItem>(new CartItem{CartId = cart.Id, ProductId = product.Id});
            unitOfWork.Update<Product>(x => x.Id == product.Id, product => { product.OrderCount++; });
        }
    }
}


public class UnitOfWork : IDisposable
{
    private readonly DataContext _dataContext;
    private readonly Func<Exception, bool> _handleException;
    private bool _dirty;

    public UnitOfWork(DataContext dataContext, Func<Exception,bool> handleException)
    {
        _dataContext = dataContext;
        _handleException = handleException;
    }

    private Table<T> Table<T>()
        where T: class
    {
        return _dataContext.GetTable<T>();
    }
    private T[] Find<T>(Expression<Func<T,bool>> select)
        where T: class
    {
        return Table<T>().Where(select).ToArray();
    }

    public void Create<T>(T persistentObject)
        where T: class 
    {
        Table<T>().InsertOnSubmit(persistentObject);
        _dirty = true;
    }
    public void Update<T>(Expression<Func<T, bool>> select, Action<T> update)
        where T : class
    {
        var items = Find<T>(select);
        if (items.Length > 0)
        {
            foreach (var target in items) update(target);
            _dirty = true;
        }
    }
    public void Delete<T>(Expression<Func<T, bool>> select)
        where T : class 
    {
        var items = Find<T>(select);
        switch (items.Length)
        {
            case 0: return;
            case 1:
                Table<T>().DeleteOnSubmit(items[0]);
                break;
            default:
                Table<T>().DeleteAllOnSubmit(items);
                break;
        }
        _dirty = true;
    }

    public void Dispose()
    {
        if (_dirty)
        {
            Commit(1);
        }
    }

    private void Commit(int attempt)
    {
            try
            {
                _dataContext.SubmitChanges();
            }
            catch (Exception exception)
            {
                if (attempt == 1 && _handleException != null && _handleException(exception))
                {
                    Commit(2);
                }
                else
                {
                    throw;
                }
            }
    }
}  
smartcaveman
  • 41,281
  • 29
  • 127
  • 212

2 Answers2

5

Because an unhandled exception will commit the transaction. And an exception implies that something did not go as planned = transaction should not be committed.

It's far better to Rollback in Dispose if Commit has not been called before disposing.

jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • @jgauffin, take a look at my `Dispose()` method. I don't think that would be a problem. – smartcaveman May 12 '11 at 08:05
  • +1, that's what I was going to answer. It's much better to implicitly rollback. Plus, it's consistent with other APIs (e.g. ADO.NET transactions) – Thomas Levesque May 12 '11 at 08:09
  • a) Do you never have any logic inside the `using` block other than UOW statements? If so, those could also generate exceptions => commit. b) Since I can't see how `unitOfWork.Create` works, I cannot really say how you handle exceptions in those. – jgauffin May 12 '11 at 08:09
  • @smartcaveman: what if you need to perform an Update, then something else, then a Delete? If the Update completes without error, then an error occurs while doing "something else", the Delete won't be executed, but you will still commit the UnitOfWork – Thomas Levesque May 12 '11 at 08:11
  • @jgauffin, (1) I see what you are saying now - if something else threw an exception then it should be rolled back, and my defined behavior is to commit regardless if it is dirty. (2) `unitOfWork.Create` is in the source I posted as well – smartcaveman May 12 '11 at 08:15
  • @Thomas, I see your point - I think that is what @jgauffin was explaining as well. Thank you guys for this – smartcaveman May 12 '11 at 08:16
  • 3
    I think at the end of the day, you're not going to be the only one reading your code so you need to design your code to show intent. I think it's more obvious what's going on to others (and to yourself in a week from now) with a "visible" Commit method immediately following your transaction code. – enamrik May 12 '11 at 08:17
0

What if one of your functions you call within the using block throws an exception? It might leave your unit of work in an inconsistent/incomplete state which you then commit.

ChrisWue
  • 18,612
  • 4
  • 58
  • 83