The Unit of Work design pattern is a software design pattern that is widely used in software development. It is a way to group database-related operations into a single transaction or “unit of work” to maintain consistency and integrity in the database.
This article will discuss the Unit of Work pattern, its benefits, and how to implement it in C#.
The Unit of Work pattern is a way to encapsulate a set of database-related operations into a single transaction. This is done to ensure that all the functions are committed or rolled back together, ensuring consistency in the database.
It also allows developers to batch multiple operations, reducing the number of database trips and improving the application’s performance.
There are several benefits of using the Unit of Work pattern, including:
Consistency: The Unit of Work pattern ensures that all related operations are committed or rolled back together, ensuring consistency in the database.
Performance: The Unit of Work pattern reduces the number of database trips, improving the application’s performance.
Encapsulation: The Unit of Work pattern encapsulates database-related operations, making it easier to manage database transactions.
Abstraction: The Unit of Work pattern abstracts the database layer, making it easier to change the database implementation without affecting the application layer.
We will use the Repository pattern to implement the Unit of Work pattern in C#. The Repository pattern is a way to encapsulate the data access layer and provides a layer of abstraction between the application and the database.
The first step is to define the Unit of Work interface, which will define the contract for the database operations.
public interface IUnitOfWork
{
void Commit();
void Rollback();
}
The next step is to implement the Unit of Work interface. In the implementation, we will create a new instance of the DbContext for each unit of work.
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _dbContext;
public UnitOfWork(DbContext dbContext)
{
_dbContext = dbContext;
}
public void Commit()
{
_dbContext.SaveChanges();
}
public void Rollback()
{
// Implement rollback logic
}
}
The next step is to define the Repository interface which will determine the contract for the data access operations.
public interface IRepository<T> where T : class
{
void Add(T entity);
void Remove(T entity);
void Update(T entity);
}
The next step is to implement the Repository interface. We will use the Unit of Work to manage the database transactions in the implementation.
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbSet<T> _dbSet;
private readonly IUnitOfWork _unitOfWork;
public Repository(DbSet<T> dbSet, IUnitOfWork unitOfWork)
{
_dbSet = dbSet;
_unitOfWork = unitOfWork;
}
public void Add(T entity)
{
_dbSet.Add(entity);
}
public void Remove(T entity)
{
_dbSet.Remove(entity);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public void SaveChanges()
{
_unitOfWork.Commit();
}
}
The final step is to use the Repository and Unit of Work in the application.
In the banking application, we have to implement various database operations, such as adding a new customer, transferring money from one account to another, and retrieving account details. We will use the Unit of Work pattern to group these operations into a single transaction.
We will define the Unit of Work interface, which will determine the contract for the database operations.
public interface IUnitOfWork
{
void Commit();
void Rollback();
IRepository<Customer> Customers { get; }
IRepository<Account> Accounts { get; }
IRepository<Transaction> Transactions { get; }
}
We will implement the Unit of Work interface. In the implementation, we will create a new instance of the DbContext for each unit of work.
public class UnitOfWork : IUnitOfWork
{
private readonly BankingDbContext _dbContext;
private IRepository<Customer> _customers;
private IRepository<Account> _accounts;
private IRepository<Transaction> _transactions;
public UnitOfWork(BankingDbContext dbContext)
{
_dbContext = dbContext;
}
public IRepository<Customer> Customers
{
get
{
if (_customers == null)
{
_customers = new Repository<Customer>(_dbContext.Customers, this);
}
return _customers;
}
}
public IRepository<Account> Accounts
{
get
{
if (_accounts == null)
{
_accounts = new Repository<Account>(_dbContext.Accounts, this);
}
return _accounts;
}
}
public IRepository<Transaction> Transactions
{
get
{
if (_transactions == null)
{
_transactions = new Repository<Transaction>(_dbContext.Transactions, this);
}
return _transactions;
}
}
public void Commit()
{
_dbContext.SaveChanges();
}
public void Rollback()
{
// Implement rollback logic
}
}
We will define the Repository interface and determine the contract for the data access operations.
public interface IRepository<T> where T : class
{
void Add(T entity);
void Remove(T entity);
void Update(T entity);
IEnumerable<T> GetAll();
T GetById(int id);
}
We will implement the Repository interface. We will use the Unit of Work to manage the database transactions in the implementation.
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbSet<T> _dbSet;
private readonly IUnitOfWork _unitOfWork;
public Repository(DbSet<T> dbSet, IUnitOfWork unitOfWork)
{
_dbSet = dbSet;
_unitOfWork = unitOfWork;
}
public void Add(T entity)
{
_dbSet.Add(entity);
}
public void Remove(T entity)
{
_dbSet.Remove(entity);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public IEnumerable<T> GetAll()
{
return _dbSet.ToList();
}
public T GetById(int id)
{
return _dbSet.Find(id);
}
public void SaveChanges()
{
_unitOfWork.Commit();
}
}
We will use the application's Repository and Unit of Work to perform various database operations.
For example, to add a new customer, we can create a new instance of the Customer class, add it to the Customers repository, and then call the SaveChanges method of the repository to commit the changes to the database. Here’s an example code snippet:
var unitOfWork = new UnitOfWork(new BankingDbContext());
var customer = new Customer
{
FirstName = "John",
LastName = "Doe",
Email = "[email protected]"
};
unitOfWork.Customers.Add(customer);
unitOfWork.Customers.SaveChanges();
Similarly, we can use the Unit of Work and Repository to perform other database operations, such as transferring money from one account to another, retrieving account details, etc.
Using the Unit of Work pattern, we can group these operations into a single transaction, ensuring that all operations succeed or fail together.
This article discusses the Unit of Work pattern and how it can be used in C# applications to manage database transactions. We have seen how the design helps to group database operations into a single transaction and ensures that all the operations succeed or fail together.
Using the Unit of Work pattern, we can improve the performance and scalability of our applications and make them more robust and reliable.
C# Publication, LinkedIn, Instagram, Twitter, Dev.to, BuyMeACoffee
Also published here