user9124444
user9124444

Reputation:

How should the UnitOfWork pattern be actually handled?

What should a UoW be actually handled?

The way I see it, the UoW should not be handling commits and rollbacks. It should just provide means to do so, and should only be responsible with tracking changes on objects that are about to be committed such that if they are in some inconsistent state or something has changed, the transaction should fail?

So the way I see a UoW is something like this.

public interface IUnitOfWork : IDisposable
{
    IAtomicTransaction BeginTransaction();

    Task<object> SaveAsync<TEntity>(TEntity entity);

    Task UpdateAsync<TEntity>(TEntity entity);

    Task RemoveAsync<TEntity>(TEntity entity);
}

Committing should be something like this:

interface IAtomicTransaction : IDisposable
{
    void Commit();

    void Rolleback();
}

Take for example this post (and not only this but there are many like it),

Unit of Work + Repository Pattern in MVC5 and Web API 2 with Fluent NHibernate and Ninject

If you look you will see that it uses ISession within the repository, which I find it to be a mistake since it will directly link the Repository (your business) to NHibernate's ISession. Shouldn't the UoW take this responsibility? Should you start changing your business implementation because you change the framework? Shouldn't the UoW act as an Adapter, something like an anti-corruption layer?

Upvotes: 1

Views: 955

Answers (2)

Amit Joshi
Amit Joshi

Reputation: 16409

As now you have edited your question, I have to change the way I answer; hence the second answer. My first answer is still valuable (hopefully ;)).

UoW should automatically figure out what (if any) changes needs to be flushed.

IAtomicTransaction BeginTransaction();
void Commit();
void Rolleback();

The above methods may or may not be the part of UoW. Many implementations of UoW expose those. The drawback of exposing those is that, transaction handling becomes responsibility of the caller; not your class. The plus point is that, the caller gets better control over the process.

If you want to bypass the drawback, see the alternative in my first answer.

Task<object> SaveAsync<TEntity>(TEntity entity);
Task UpdateAsync<TEntity>(TEntity entity);
Task RemoveAsync<TEntity>(TEntity entity);

The above methods are part of Repositories. Those cannot be the part of UoW. UoW should automatically figure out what to do based on change tracking. If we limit our discussion with just database transaction, then DbTransaction handles this correctly. For more detailed ways to handle change tracking, please refer to my first answer.

Following is the NHibernate-based implementation. Please note that this is not recommended. If you are using a full ORM, you should avoid these type of implementations as they add little value to your design. If you simply replace ISession with IDbConnection, this could be implemented with ADO.NET as well

public interface IUnitOfWork : IDisposable
{
    void Flush();
}

public sealed class UnitOfWork : IUnitOfWork
{
    public UnitOfWork()
    {
        session = SessionFactroyHelper.CreateSession();
        transaction = session.BeginTransaction();
    }

    ISession session = null;
    ITransaction transaction = null;

    void IUoWSession.Flush()
    {
        transaction.Commit();
    }

    void IDisposable.Dispose()
    {
        transaction.Dispose();
        transaction = null;
        session.Dispose();
        session.Dispose();
    }
}

By the way, this topic itself is opinion-based. How to implement UoW and Repositories is an individual design decision. If you are really keen about implementing correct(?) UoW, consider using some advanced ORM directly in the code bypassing the UoW wrapper.

Upvotes: 0

Amit Joshi
Amit Joshi

Reputation: 16409

UoW is more than just a transaction. Following is the quote from Martin Fowler:

A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done, it figures out everything that needs to be done to alter the database as a result of your work.

It should:

  1. Provide a context for your repositories
    Either allow to create repositories from this instance of UoW or allow to inject this instance of UoW in repository. I prefer first approach. All repositories with internal constructor and some factory-like method in UoW creates repositories.
  2. Track the changes in this context
    This is complex topic but let us limit this to database transaction for simplicity. Many ORMs handle this in better way. Tracking the state of Entity objects can be done in multiple ways. Maintain the list of changes, maintain the state (dirty or not) of entity or maintain the original copy of entity and compare it with final copy at the end, etc.
  3. Flush/Don't-Flush the changes done in this context
    This is linked to above point. Based on tracking, decide what changes are need to be flushed to storage. Some UoW implementations also support auto-flush where UoW automatically decide when to flush the changes. Changes are flushed if everything was fine; not flushed if there was a problem. Again, let us limit this to database transaction for simplicity. For detailed implementations, using some good ORM is better solution.
  4. Create and Cleanup resources
    UoW instance should create (automatically or manually) the resources necessary to carry out the actions and clean them up when no longer needed.

interface IUnitOfWork : IDisposable
{
    IDbConnection Connection { get; }
    IDbTransaction Transaction { get; }
    void Begin();
    void Commit();
    void Rollback();

    IRepository CreateRepository(....);
}

Method CreateRepository creates an instance of repository under this UoW. This way, you may share same UoW across multiple repositories. This way, one DB transaction can be spread across multiple repositories. Other alternative is to inject UoW in repository as shown here.

The problem with this approach is that, it does not force UoW. It is up to caller when (or whether) to start transaction.


Other mini-version of UoW (that forces the UoW) I can imagine is something like below:

public sealed class UoWSession
{
    public UoWSession()
    {
        // Open connection here
        // Begin transaction here
    }

    IRepository CreateRepository(....)
    {
        // Create and return the requested repository instance here
    }

    void Commit()
    {
        transaction.Commit();
    }

    void Dispose()
    {
        // If transaction is not committed, rollback it here.
        // Clean up resources here.
    }
}

Without using an ORM, you have to expose something that tells you that everything was fine. Above implementation uses Commit method.
There may be a simple property, let’s say IsAllWell, which is false by default and calling code set it explicitly. Your Dispose method then commits or rolls back the transaction based on property. In this case, you do not need to expose Commit method as you are handling it internally on the flag.

Upvotes: 0

Related Questions