Saturday, Jul 21, 2018
Unit of Work - a Design Pattern
A design pattern is a general reusable solution to a commonly occurring problem in software development. It cannot be transformed directly into the source code as a finished design since it is more of a template that can be used in many different situations. Unit of work is one of them - and in this post, we’ll explain how to use it and how to implement it in ASP.NET using Entity Framework.
When to use Unit of Work?
Imagine you have users and roles, and it is important to ensure that all users have roles assigned to them. This is where Unit of Work comes in. It helps us save everything, user and his roles - or nothing, if something fails. Unit of Work is mostly used when we want to get a set of actions happen, and if one action fails, to cancel all of them.
In general, Unit of Work has two important tasks:
- to keep a list of requests in one place (the list can contain multiple insert, update, and delete requests), and
- to send that requests as one transaction to the database.
An example implementation based on Entity Framework
For the DbContext
variable, a new context is instantiated in the following way:
protected IMyDbContext DbContext { get; private set; }
and this is a constructor that uses the DbContext
variable:
public UnitOfWork(IMyDbContext dbContext)
{
if (dbContext == null)
{
throw new ArgumentNullException("DbContext");
}
DbContext = dbContext;
}
Items will be added using this method:
public Task<int> AddAsync<T>(T entity) where T : class
{
DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
if (dbEntityEntry.State != EntityState.Detached)
{
dbEntityEntry.State = EntityState.Added;
}
else
{
DbContext.Set<T>().Add(entity);
}
return Task.FromResult(1);
}
If we want to edit (update) items, we need the update method:
public Task<int> UpdateAsync<T>(T entity) where T : class
{
DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
if (dbEntityEntry.State == EntityState.Detached)
{
DbContext.Set<T>().Attach(entity);
}
dbEntityEntry.State = EntityState.Modified;
return Task.FromResult(1);
}
To delete item(s) we will use following:
public Task<int> DeleteAsync<T>(T entity) where T : class
{
DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
if (dbEntityEntry.State != EntityState.Deleted)
{
dbEntityEntry.State = EntityState.Deleted;
}
else
{
DbContext.Set<T>().Attach(entity);
DbContext.Set<T>().Remove(entity);
}
return Task.FromResult(1);
}
public Task<int> DeleteAsync<T>(string ID) where T : class
{
var entity = DbContext.Set<T>().Find(ID);
if (entity == null)
{
return Task.FromResult(0);
}
return DeleteAsync<T>(entity);
}
Note: This implementation is convenient because I used string ID
in the database. You can use Guid
or int Id
or whatever suits you.
Besides the basic create, update, and delete methods described here, to commit the transaction, we will need a CommitAsync
method:
public async Task<int> CommitAsync()
{
int result = 0;
using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
result = await DbContext.SaveChangesAsync();
scope.Complete();
}
return result;
}
Finally, the method to dispose the whole transaction if any of the actions fail:
public void Dispose()
{
DbContext.Dispose();
}
For all that methods we usually create the interface like this:
public interface IUnitOfWork : IDisposable
{
Task<int> AddAsync<T>(T entity) where T : class;
Task<int> CommitAsync();
Task<int> DeleteAsync<T>(T entity) where T : class;
Task<int> DeleteAsync<T>(string ID) where T : class;
Task<int> UpdateAsync<T>(T entity) where T : class;
}
And here is the Unit of Work factory that creates it:
public interface IUnitOfWorkFactory
{
IUnitOfWork CreateUnitOfWork();
}
Note: I used Ninject Factory extension here, so I didn’t have to implement it.
How to use it
All you need to do now is to create the Unit of Work in the method where you need it:
public class MyClass
{
IUnitOfWorkFactory uowFactory;
public MyClass(IUnitOfWorkFactory uowFactory)
{
this.uowFactory = uowFactory;
}
public async Task<int> MyMethod()
{
// Creates the Unit of Work
var unitOfWork = uowFactory.CreateUnitOfWork();
}
}
Note: MyMethod
should be async because of CommitAsync
, AddAsync
and other async methods.
Next, add requests to unitOfWork
in MyMethod
in order in which you want them to perform. For example, to add user and then add his roles:
// Insert user into user table
var userId = await unitOfWork.AddAsync(user);
// Multiple inserts for user roles
foreach (var userRole in userRoles)
{
// Assign UserId before the insert
userRole.UserId = userId.Value;
// Insert into user role table
await unitOfWork.AddAsync(userRole);
}
Finally, all you need to call is:
// Commit
await unitOfWork.CommitAsync();
All requests added to the Unit of Work will be sent to the database when you call CommitAsync
. Since the Unit of Work is handling them all in the TransactionScope
, it will assure that no unfinished work will make changes to our data - either all actions will be successful, or no changes will be done at all.