Unit-of-Work-Design-Pattern
What is the use of Unit of Work design pattern?
Unit of Work design pattern does two important things: first it maintains in-memory updates and second it sends these in-memory updates as one transaction to the database.
So to achieve the above goals it goes through two steps:
- It maintains lists of business objects in-memory which have been changed (inserted, updated, or deleted) during a transaction.
- Once the transaction is completed, all these updates are sent as one big unit of work to be persisted physically in a database in one go.
What is “Work” and “Unit” in a software application?
A simple definition of Work means performing some task. From a software application perspective Work is nothing but inserting, updating, and deleting data. For instance let’s say you have an application which maintains customer data into a database.
So when you add, update, or delete a customer record on the database it’s one unit. In simple words the equation is.
1 customer CRUD = 1 unit of work
Where CRUD stands for create, read, update, and delete operation on a single customer record.
Logical transaction! = Physical CRUD
The equation which we discussed in the previous section changes a lot when it comes to real world scenarios. Now consider the below scenario where every customer can have multiple addresses. Then many rows will become 1 unit of work.
For example you can see in the below figure customer “Shiv” has two addresses, so for the below scenario the equation is:
3 Customer CRUD = 1 Logical unit of work
So in simple words, transactions for all of the three records should succeed or all of them should fail. It should be ATOMIC. In other words it’s very much possible that many CRUD operations will be equal to 1 unit of work.
So this can be achieved by using simple transactions?
Many developers can conclude that the above requirement can be met by initiating all the CRUD operations in one transaction. Definitely under the cover it uses database transactions (i.e., TransactionScope
object). But unit of work is much more than simple database transactions, it sends only changes and not all rows to the database.
Let me explain to you the same in more detail.
Let’s say your application retrieves three records from the database. But it modifies only two records as shown in the below image. So only modified records are sent to the database and not all records. This optimizes the physical database trips and thus increases performance.
In simple words the final equation of unit of work is:
1 Unit of work = Modified records in a transaction
So how can we achieve this?
To achieve the same the first thing we need is an in-memory collection. In this collection, we will add all the business objects. Now as the transaction happens in the application they will be tracked in this in-memory collection.
Once the application has completed everything it will send these changed business objects to the database in “one transaction”. In other words either all of them will commit or all of them will fail.
C# Sample code for unit of work
Step 1: Create a generalized interface (IEntity) for business objects
At the end of the day a unit of work is nothing but a collection which maintains and track changes on the business objects. Now in an application we can have lots of business object with different types. For example you can have customer, supplier, accounts etc. In order to make the collection reusable across any business object let’s generalize all business objects as just “Entities”.
With this approach we make this collection reusable with any business object and thus avoid any kind of duplicate code.
So the first step is to create a generalized interface called IEntity
which represents a business object in our project.
This IEntity
interface will have an ID
property and methods (insert, update, delete, and load) which will help us to do the CRUD operation on the business object. The ID
property is a unique number which helps us uniquely identify the record in a database.
public interface IEntity
{
int Id { set; get; }
void Insert();
void Update();
List<IEntity> Load();
}
Step 2: Implement the IEntity interface
The next step is to implement the IEntity
in all our business objects. For example you can see below a simpleCustomer
business object which implements the IEntity
interface. It also implements all CRUD operations. You can see that it makes a call to the data access layer to implement insert, update, and load operations.
public class Customer : IEntity
{
private int _CustomerCode = 0;
public int Id
{
get { return _CustomerCode; }
set { _CustomerCode = value; }
}
private string _CustomerName = "";
public string CustomerName
{
get { return _CustomerName; }
set { _CustomerName = value; }
}
public void Insert()
{
DataAccess obj = new DataAccess();
obj.InsertCustomer(_CustomerCode, CustomerName);
}
public List<IEntity> Load()
{
DataAccess obj = new DataAccess();
Customer o = new Customer();
SqlDataReader ds = obj.GetCustomer(Id);
while (ds.Read())
{
o.CustomerName = ds["CustomerName"].ToString();
}
List<IEntity> Li = (new List<Customer>()).ToList<IEntity>();
Li.Add((IEntity) o);
return Li;
}
public void Update()
{
DataAccess obj = new DataAccess();
obj.UpdateCustomer(_CustomerCode, CustomerName);
}
}
I am not showing the data access code in the article to avoid confusion. You can always download the full code which is attached with the article.
Step 3: Create the unit of work collection
The next step is to create the unit of work collection class. So below is a simple class we have created calledSimpleExampleUOW
.
This class has two generic collections defined as a class level variable: Changed
and New
. The Changed
generic collection will store entities which are meant for updates. The New
generic collection will have business entities which are fresh / new records.
Please note in this demo I have not implemented the delete functionality. I leave that to you as a home work .
public class SimpleExampleUOW
{
private List<T> Changed = new List<T>();
private List<T> New = new List<T>();
…..
…..
}
We have also implemented the Add
and Load
methods which load the New
and Changed
collections, respectively. If the object is entered via the Add
method it gets added to the New
collection. If it’s loaded from the database it gets loaded in the Changed
collection.
public void Add(T obj)
{
New.Add(obj);
}
public void Load(IEntity o)
{
Changed = o.Load() as List<T>;
}
Now all these changes which are updated in the collection also need to be sent in one go to the database. So for that we have also created a Commit
function which loops through the New
and Changed
collections and callsInsert
and Update
, respectively. We have also used the TransactionScope
object to ensure that all these changes are committed in an atomic fashion.
Atomic fashion means either the entire changes are updated to the database or none of them are updated.
public void Committ()
{
using (TransactionScope scope = new TransactionScope())
{
foreach (T o in Changed)
{
o.Update();
}
foreach (T o in New)
{
o.Insert();
}
scope.Complete();
}
}
Below is how the full code of SimpleExampleUOW
looks like:
public class SimpleExampleUOW
{
private List<IEntity> Changed = new List<IEntity>();
private List<IEntity> New = new List<IEntity>();
public void Add(IEntity obj)
{
New.Add(obj);
}
public void Committ()
{
using (TransactionScope scope = new TransactionScope())
{
foreach (IEntity o in Changed)
{
o.Update();
}
foreach (IEntity o in New)
{
o.Insert();
}
scope.Complete();
}
}
public void Load(IEntity o)
{
Changed = o.Load() as List<IEntity>;
}
}
Step 4: See it working
On the client side we can create the Customer
object, add business objects in memory, and finally all these changes are sent in an atomic manner to the physical database by calling the Commit
method.
Customer Customerobj = new Customer();// record 1 Customer
Customerobj.Id = 1000;
Customerobj.CustomerName = "shiv";
Supplier SupplierObj = new Supplier(); // Record 2 Supplier
Supplierobj.Id = 2000;
Supplierobj.SupplierName = "xxxx";
SimpleExampleUOW UowObj = new SimpleExampleUOW();
UowObj.Add(Customerobj); // record 1 added to inmemory
UowObj.Add(Supplierobj); // record 1 added to inmemory
UowObj.Committ(); // The full inmemory collection is sent for final committ
Leave a Reply