To explain this definition, let’s take a look at this code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using Unit_Of_Work.DataModel; | |
namespace Unit_Of_Work | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
EmployeeContext dbContext = new EmployeeContext(); | |
dbContext.Departments.Add(new Department | |
{ DepartmentName = "Customer Service" }); | |
dbContext.Departments.Add(new Department | |
{ DepartmentName = "Legal", | |
Employees = { | |
new Employee{EmployeeName="Eddie More"}, | |
new Employee{EmployeeName="Mary Joe"}} | |
} | |
); | |
dbContext.Employees.Add(new Employee | |
{Department_Id = 1, EmployeeName = "John Smith"}); | |
dbContext.SaveChanges(); | |
foreach (var department in dbContext.Departments) | |
{ | |
Console.WriteLine(String.Format("{0} {1}", | |
department.Id, department.DepartmentName)); | |
foreach (var Employee in department.Employees) | |
{ | |
Console.WriteLine(String.Format(" {0} {1}", | |
Employee.Department_Id, Employee.EmployeeName)); | |
} | |
} | |
Console.ReadKey(); | |
} | |
} | |
} |
This is the concept of Unit of Work Design Pattern. The dbContext is simply an aggregator that handles its child entities in terms of inserting, reading, updating and deleting in-memory records and committing those records at once while resolving concurrency issues.
Think of a database that has tables in it. The database is the aggregate root to its tables.
Why is it called Unit of Work?
From the application designer's point of view, a Unit of Work is a sequence of actions that needs to be complete before any of the individual actions can be regarded as complete. To ensure data integrity, a unit of work must be atomic, consistent, isolated, and durable.
Actions such as inserting several records to 1 or more entities before finally commit/save those records is 1 unit of work.
Why do we use Unit of Work?
- Track changes in persistent objects
Efficient data access, manage concurrency problems, and manage transactions.
- Logical Transactions
In a web application where bandwidth is expensive (most especially in mobile devices), as much as possible we want limit multiple round trips to perform updates on a data store. This is all about efficiency. All computational requirements or steps involved will be completed first before final commit will happen to the data store.
- Testability
While it is true that DbContext is enough to implement Unit of Work, the problem is testability. We need several abstractions to solve this problem.
In this example, we are going to build a simple payroll using Console Project. This is of course the way to do payroll but we use it the purpose of demonstrating Unit of Work.
What does it do?
- It can add employees with the corresponding department, it could be in batch and commit at once;
- Compute Net Pay in batch and commit at once.
- Visual Studio 2013
- C#
- EF 6.1 Database First approach
- LINQ
- SQL Server
- Repository Pattern using Generic
- Dependency Injection
Entity Data Model (EmployeeModel)
UML Class Diagram
Participants
Interface/Class | Function |
---|---|
IUnitOfWork | An interface that defines the methods and entities to be implementation by SQLUnitOfWork |
IGenericRepository | An interface that defines the methods CRUD methods to be implemented by the entities(SQLRepository). Generic Repository is best when there are repetitive data access of different entitites. |
SQLUnitOfWork | Concrete implementation of IUnitOfWork. |
SQLRepository | Concrete implementation of IGenericRepository. |
EmployeeContext | The Data Model of Employees (Entity Data Model) . |
Department | A POCO Class for storing Department data. |
Employee | A POCO Class for storing Employee data. |
Payroll | A POCO Class for storing Payroll data. |
The Codes
IUnitOfWork.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Unit_Of_Work.DataModel; | |
namespace Unit_Of_Work.Interface | |
{ | |
public interface IUnitOfWork | |
{ | |
IGenericRepository<Payroll> EmployeePayroll { get; } | |
IGenericRepository<Department> Departments { get; } | |
IGenericRepository<Employee> Employees { get; } | |
void ComputePayroll(); | |
void Commit(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
namespace Unit_Of_Work.Interface | |
{ | |
public interface IGenericRepository<T> where T : class | |
{ | |
IQueryable<T> Query(); | |
T GetById(int id); | |
void Add(T entity); | |
void Update(T entity); | |
void Delete(T entity); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Data.Entity; | |
using System.Linq; | |
using Unit_Of_Work.DataModel; | |
using Unit_Of_Work.Interface; | |
namespace Unit_Of_Work.EFPayroll | |
{ | |
public class SQLRepository<T> : IGenericRepository<T> where T : class | |
{ | |
internal EmployeeContext Context; | |
internal DbSet<T> dbSet; | |
public SQLRepository(EmployeeContext _Context) | |
{ | |
Context = _Context; | |
dbSet = Context.Set<T>(); | |
} | |
public IQueryable<T> Query() | |
{ | |
IQueryable<T> query = dbSet; | |
return dbSet; | |
} | |
public T GetById(int id) | |
{ | |
return dbSet.Find(id); | |
} | |
public void Add(T entity) | |
{ | |
dbSet.Add(entity); | |
} | |
public void Update(T entity) | |
{ | |
dbSet.Attach(entity); | |
Context.Entry(entity).State = System.Data.Entity.EntityState.Modified; | |
} | |
public void Delete(T entity) | |
{ | |
if (Context.Entry(entity).State == EntityState.Detached) | |
{ | |
dbSet.Attach(entity); | |
} | |
dbSet.Remove(entity); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Unit_Of_Work.DataModel; | |
using Unit_Of_Work.Interface; | |
namespace Unit_Of_Work.EFPayroll | |
{ | |
public class SQLUnitOfWork : IUnitOfWork | |
{ | |
internal EmployeeContext Context; | |
internal SQLRepository<Department> _Departments; | |
internal SQLRepository<Employee> _Employees; | |
internal SQLRepository<Payroll> _Payroll; | |
public SQLUnitOfWork(EmployeeContext _Context) | |
{ | |
Context = _Context; | |
} | |
public IGenericRepository<Payroll> EmployeePayroll | |
{ | |
get | |
{ | |
if (this._Payroll == null) | |
{ | |
this._Payroll = new SQLRepository<Payroll>(Context); | |
} | |
return this._Payroll; | |
} | |
} | |
public IGenericRepository<Department> Departments | |
{ | |
get { | |
if (this._Departments == null) | |
{ | |
this._Departments = new SQLRepository<Department>(Context); | |
} | |
return this._Departments; | |
} | |
} | |
public IGenericRepository<Employee> Employees | |
{ | |
get | |
{ | |
if (this._Employees == null) | |
{ | |
this._Employees = new SQLRepository<Employee>(Context); | |
} | |
return this._Employees; | |
} | |
} | |
public void ComputePayroll() | |
{ | |
decimal NetPay = 0; | |
decimal Salary = 0; | |
decimal StateTax = 0; | |
decimal StateInsurance = 0; | |
double StateTaxPercent = .30; | |
double StateInsurancePercent = .05; | |
foreach (var employee in this.Employees.Query()) | |
{ | |
Salary = employee.Salary.Value; | |
StateTax = Salary * (decimal)StateTaxPercent; | |
StateInsurance = Salary * (decimal)StateInsurancePercent; | |
NetPay = Salary - (StateTax + StateInsurance); | |
employee.Payrolls.Add(new Payroll {StateTax = StateTax, StateInsurance = StateInsurance, NetPay = NetPay }); | |
} | |
} | |
public void Commit() | |
{ | |
Context.SaveChanges(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using Unit_Of_Work.DataModel; | |
using Unit_Of_Work.EFPayroll; | |
using Unit_Of_Work.Interface; | |
using System.Collections.Generic; | |
namespace Unit_Of_Work | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
IUnitOfWork UnitOfWork = new SQLUnitOfWork(new EmployeeContext()); | |
IList<Employee> _Employees = new List<Employee>(); | |
_Employees.Add(new Employee { EmployeeName = "Jim Lee", Salary = 6000 }); | |
_Employees.Add(new Employee { EmployeeName = "Jannet Smith", Salary = 7000 }); | |
_Employees.Add(new Employee { EmployeeName = "Leonor Jordan", Salary = 8000 }); | |
UnitOfWork.Departments.Add(new Department | |
{ | |
DepartmentName = "R&D", | |
Employees = _Employees | |
}); | |
UnitOfWork.Commit(); | |
UnitOfWork.ComputePayroll(); | |
UnitOfWork.Commit(); | |
//Let Display the computed salaries to test if it works. | |
//You can refactor this and have it as method of IUnitWork. | |
//This is for demo purpose only. | |
var _Department = ""; | |
var _EmployeeName = ""; | |
decimal _Salary = 0; | |
decimal _StateTax = 0; | |
decimal _StateInsurance = 0; | |
decimal _NetPay = 0; | |
Console.WriteLine("======================================================================="); | |
foreach (var department in UnitOfWork.Departments.Query()) | |
{ | |
_Department = department.DepartmentName; | |
foreach (var employee in department.Employees) | |
{ | |
_EmployeeName = employee.EmployeeName; | |
_Salary = (decimal)employee.Salary; | |
foreach (var payroll in employee.Payrolls) | |
{ | |
_StateTax = (decimal)payroll.StateTax; | |
_StateInsurance = (decimal)payroll.StateInsurance; | |
_NetPay = (decimal)payroll.NetPay; | |
Console.WriteLine(String.Format("{0} | {1} | {2:C2} | {3:C2} | {4 :C2} | {5:C2}", _Department, _EmployeeName, _Salary, _StateTax, _StateInsurance, _NetPay)); | |
} | |
} | |
} | |
Console.WriteLine("======================================================================="); | |
} | |
} | |
} |
Your comments please!