Prerequisites: Repository Pattern
What is Unit of Work Pattern and Why Is It Needed??
According to Martin Fowler: ‘It maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems’.
When you’re pulling data in and out of a database, it’s important to keep track of what you’ve changed; otherwise, that data won’t be written back into the database. Similarly, you have to insert new objects you create and remove any objects you delete.
You can change the database with each change to your object model, but this can lead to lots of very small database calls, which end up being very slow. Furthermore, it requires you to have a transaction open for the whole interaction, which is impractical if you have a business transaction that spans multiple requests. The situation is even worse if you need to keep track of the objects you’ve read so you can avoid inconsistent reads.
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.
Unit of work shows up in all the persistence tools available.The ITransaction
interface in NHibernate, the DataContext
class in LINQ to SQL, and the ObjectContext
class in the Entity Framework are all examples of a Unit of Work. For that matter, the venerable DataSet
can be used as a Unit of Work.
Here in this example, we will go ahead with the existing Repository pattern code and try to implement the Unit of work pattern with a Singleton Session pattern. Ideally, the Session should hold all the information about the Repositories as well as the Unit of work. Unit of work is ideal for tracking the object which the Repositories hold, thus minimizing the round trips.
This example is just for understanding how Unit of work gets implemented in any ORM. As depending on the necessity of the application, if you ever were to build a custom ORM, you could use this approach.
We will be going ahead with the same database structure:
Database structure
When you view code in the dbml file, a partial file is created which allows you to enter custom code. Alternatively, you can also double click on Product
entity which generates the same below file (as we are only working with Product Entities). We will write some properties for them as below.
using System.Collections.Generic;
namespace Xamlized.Database
{
partial class CustomersDataContext
{
}
partial class Product
{
public bool IsDirty { get; set; }
public bool IsNew { get; set; }
public bool IsDeleted { get; set; }
}
}
As seen in the above code, we have created change tracking properties, viz. IsDirty
, IsNew
, IsDeleted
.
IsDirty
: This property is used to tell the Unit of Work that the object has some changes, so that while the commit only the properties which are changed actually goes in the SQL query. IsNew
: This property is used to indicate that the Entity is new to the Unit of work. IsDeleted
: This property is used to indicate that the Entity is deleted to the Unit of work.
We will now look at the Unit of Work(UOW) interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Xamlized.Repositories.Interfaces
{
public interface IUnitOfWork<T>
{
void MarkDirty(T entity);
void MarkNew(T entity);
void MarkDeleted(T entity);
void SaveChanges();
}
}
Now, we will look at UOW’s implementation:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Linq;
using System.Linq.Expressions;
using Xamlized.Repositories.Interfaces;
using System.Reflection;
namespace Xamlized.Repositories.Models
{
public class UnitOfWork<T>:IUnitOfWork<T> where T:class
{
public int ID { get; set; }
internal DataContext DataContext;
internal List<T> Entities;
public UnitOfWork(DataContext dataContext)
{
DataContext = dataContext;
}
public void MarkDirty(T entity)
{
PropertyInfo prop = entity.GetType().GetProperty
("IsDirty", BindingFlags.Public | BindingFlags.Instance);
if (null != prop && prop.CanWrite)
{
prop.SetValue(entity, true, null);
}
}
public void MarkNew(T entity)
{
PropertyInfo prop = entity.GetType().GetProperty
("IsNew", BindingFlags.Public | BindingFlags.Instance);
if (null != prop && prop.CanWrite)
{
prop.SetValue(entity, true, null);
}
}
public void MarkDeleted(T entity)
{
PropertyInfo prop = entity.GetType().GetProperty
("IsDeleted", BindingFlags.Public | BindingFlags.Instance);
if (null != prop && prop.CanWrite)
{
prop.SetValue(entity, true, null);
}
}
public void SaveChanges()
{
foreach (var entity in Entities)
{
PropertyInfo prop = entity.GetType().GetProperty
("IsNew", BindingFlags.Public | BindingFlags.Instance);
if (null != prop && Convert.ToBoolean(prop.GetValue(entity, null))==true)
{
if ( prop.CanWrite)
{
prop.SetValue(entity, false, null);
}
DataContext.GetTable<T>().InsertOnSubmit(entity);
}
}
DataContext.SubmitChanges();
}
public IEnumerable<T> GetEntities(Func<T, bool> predicate = null)
{
if (predicate != null)
{
return Entities.Where(predicate).AsQueryable();
}
return Entities.AsQueryable();
}
public IEnumerable<T> GetAllEntities()
{
return Entities.AsQueryable();
}
}
}
IMPORTANT: I am creating Entities (List) and not using the DataTable
directly, as LINQ to SQL needs to Submit the changes to see the entities add/remove/update in the DataTable
.
For more information, you can see the Remarks column in the link: InsertOnSubmit
Hence, we will work with Entities and only use the DataTable
on Save as you can see above in the Save method.
In all the methods, viz, MarkDirty
, MarkNew
, MarkDeleted
, I am using Reflection to set the Properties to true, viz., IsDirty
, IsNew
, IsDeleted
respectively as this is a generic list.
Our Repository will also get changed slightly to incorporate UOW as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Xamlized.Repositories
{
public interface IRepository<T>
{
void Insert(T entity);
void Delete(T entity);
IQueryable<T> SearchFor(Func<T, bool> predicate);
IQueryable<T> GetAll();
}
}
The implementation for Repository is as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Linq;
using System.Linq.Expressions;
namespace Xamlized.Repositories.Models
{
public class Repository<T> : IRepository<T> where T : class
{
public UnitOfWork<T> UnitOfWork { get; set; }
public Repository(UnitOfWork<T> uow)
{
UnitOfWork = uow;
UnitOfWork.Entities = new List<T>();
UnitOfWork = uow;
foreach (var item in UnitOfWork.DataContext.GetTable<T>())
{
UnitOfWork.Entities.Add(item);
}
}
public void Insert(T entity)
{
UnitOfWork.Entities.Add(entity);
UnitOfWork.MarkNew(entity);
}
public void Delete(T entity)
{
UnitOfWork.Entities.Remove(entity);
}
public System.Linq.IQueryable<T> SearchFor(Func<T, bool> predicate)
{
return UnitOfWork.Entities.Where(predicate).AsQueryable();
}
public System.Linq.IQueryable<T> GetAll()
{
return UnitOfWork.Entities.AsQueryable();
}
}
}
Now let us look at the Session
class which is a singleton, where I have 2 properties for Repository and Unit of Work:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamlized.Repositories.Models;
namespace Xamlized.Repositories
{
public class Session<T> where T : class
{
public static Repository<T> ObjectStorage { get; set; }
public static UnitOfWork<T> UnitOfWork { get; set; }
private static Session<T> instance;
private Session() { }
public static Session<T> Instance
{
get
{
if (instance == null)
{
instance = new Session<T>();
}
return instance;
}
}
}
}
Now we will see the client code of this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamlized.Database;
using Xamlized.Repositories;
using Xamlized.Repositories.Models;
namespace Xamlized.RepositoryPattern
{
class Program
{
static void Main(string[] args)
{
using (var dataContext = new CustomerDataContext())
{
Session<Product>.UnitOfWork = new UnitOfWork<Product>(dataContext);
Session<Product>.ObjectStorage =
new Repository<Product>(Session<Product>.UnitOfWork);
var allentities = Session<Product>.UnitOfWork.GetAllEntities();
Console.WriteLine("-------All Products from DB-----");
foreach (var product in allentities)
{
Console.WriteLine(product.Name);
}
Product p1 = new Product();
p1.Name = "New Widget";
Session<Product>.ObjectStorage.Insert(p1);
Product p2 = new Product();
p2.Name = "New Camera";
Session<Product>.ObjectStorage.Insert(p2);
allentities = Session<Product>.UnitOfWork.GetAllEntities();
Console.WriteLine("-------All Products after insert-----");
foreach (var product in allentities)
{
Console.WriteLine(product.Name);
}
var newentites =
Session<Product>.UnitOfWork.GetEntities().Where(x => x.IsNew == true);
Console.WriteLine("-------New Products-----");
foreach (var product in newentites)
{
Console.WriteLine(product.Name);
}
Session<Product>.UnitOfWork.SaveChanges();
allentities = Session<Product>.UnitOfWork.GetAllEntities();
Console.WriteLine("-------All Products after save-----");
foreach (var product in allentities)
{
Console.WriteLine(product.Name);
}
newentites =
Session<Product>.UnitOfWork.GetEntities().Where(x => x.IsNew == true);
Console.WriteLine("-------New Products after save-----");
foreach (var product in newentites)
{
Console.WriteLine(product.Name);
}
Console.ReadLine();
}
}
}
}
As you can see that we are creating products and adding using the Repository, while adding, we will use the Unit of work to mark that entity as New. We can use UOW to retrieve/ check the track the new
, dirty
and deleted
entities.
The output is as shown below:
Unit of work pattern with repository and session
Happy coding! :)