Unit of Work Pattern in ASP.NET Core MVC using EF Core

Unit of Work Pattern in ASP.NET Core MVC using EF Core

In this article, I will discuss the Unit of Work Pattern in ASP.NET Core MVC using Entity Framework Core (EF Core) with an Example. Please read our previous article discussing How to Implement Generic and Non-Generic Repository Patterns in ASP.NET Core MVC applications using Entity Framework Core. We will also work with the same example we have worked on so far.

Unit of Work in Repository Pattern in ASP.NET Core MVC using EF Core

In ASP.NET Core MVC, when using the Entity Framework Core (EF Core) with the Repository Pattern, the Unit of Work (UoW) groups one or more operations (like insert, update, delete) into a single transaction. This pattern helps maintain consistency and ensures that all operations either complete successfully or fail altogether, which is especially useful in scenarios where data integrity is crucial across multiple operations.

Key Concepts of Unit of Work Pattern
  • Transaction Management: The UoW pattern provides a way to manage transactions, allowing you to commit a batch of operations only if all of them succeed. If any operation fails, the whole transaction can be rolled back.
  • Consistency: It ensures that the database remains consistent despite errors that might occur during the execution of any part of the transaction.
  • Connection Management: UoW handles the connection to the database, ensuring that all operations use the same database context, which is important for Entity Framework performance and consistency.
Why Do We Need the Unit of Work in Repository Design Pattern?

We have already discussed the Repository Design Pattern using ASP.NET Core MVC with Entity Framework Core. And we have discussed we can create a Generic Repository and Non-Generic Repositories. All the common operations will be implemented in the Generic Repository, and Entity Specific Unique Operations will be implemented in the Non-Generic Repository. For each entity, we need to create a Separate Non-Generic Repository.

Now, suppose we have one controller, let us say EmployeesController, which will work with multiple repositories, let’s say Employee and Department Repositories. Then we will face the problem. When the controller works with multiple repositories, in that case, both repositories will generate and maintain their own instance of the DbContext class. In such a case, if the SaveChanges method of one of the repositories fails and the SaveChanges method of the other one succeeds, it will result in database inconsistency. Without the Unit of work, this situation can be represented as shown in the below diagram.

Why Do We Need the Unit of Work in Repository Design Pattern?

This is where the concept of Unit of Work comes into the picture. We will add another layer or intermediate between the Controller and the Generic/Non-Generic Repository to overcome this problem. This layer will act as a centralized store for all the repositories. This will ensure that a transaction that spans across multiple repositories should either be completed for all entities or fail entirely, as all of them will share the same instance of the DbContext. In our example, while adding data for the Employee and Department entities in a single transaction, both will use the same DbContext instance. This situation, with the Unit of work, can be represented in the following diagram.

Unit of Work Pattern in ASP.NET Core MVC using Entity Framework Core (EF Core) with an Example

In the above representation, the operations involving both Employee and Department entities will use the same DbContext instance.

So, the Unit of Work Pattern groups one or more operations (usually database CRUD operations) into a single transaction and executes them by applying the principle of doing everything or nothing. That means the transaction will be rolled back if any of the operations fail. If all the operations are successful, then it will commit the transaction. 

Implementing Unit of Work in Repository Design Pattern

Implementing the Unit of Work with the Repository Pattern in ASP.NET Core MVC Applications using Entity Framework Core (EF Core) can significantly streamline the process of managing database transactions and ensuring the consistency of your data. Here’s a step-by-step process of how to implement Unit of Work pattern:

Creating Domain Entities:

We have already created the following entities:

Employee.cs
using System.ComponentModel.DataAnnotations;

namespace CRUDinCoreMVC.Models
{
    public class Employee
    {
        public int EmployeeId { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string Position { get; set; }
        [Display(Name ="Department Name")]
        public int DepartmentId { get; set; }

        public Department? Department { get; set; }
    }
}
Department.cs
namespace CRUDinCoreMVC.Models
{
    public class Department
    {
        public int DepartmentId { get; set; }
        public string Name { get; set; }
        public List<Employee> Employees { get; set; }
    }
}
Creating the DbContext

We have already created the following EFCoreDbContext.cs class:

using Microsoft.EntityFrameworkCore;

namespace CRUDinCoreMVC.Models
{
    public class EFCoreDbContext : DbContext
    {
        //Constructor calling the Base DbContext Class Constructor
        public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options)
        {
        }

        //OnConfiguring() method is used to select and configure the data source
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //We will store the connection string in AppSettings.json file instead of hard coding here
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }

        //Adding Domain Classes as DbSet Properties
        public DbSet<Employee> Employees { get; set; }
        public DbSet<Department> Departments { get; set; }
    }
}
Implementing the Repository Pattern:

We have already implemented both Generic and Specific Repositories as follows:

IGenericRepository.cs
namespace CRUDinCoreMVC.GenericRepository
{
    //Here, we are creating the IGenericRepository interface as a Generic Interface
    //Here, we are applying the Generic Constraint, i.e., T is going to be a class
    public interface IGenericRepository<T> where T : class
    {
        Task<IEnumerable<T>> GetAllAsync();
        Task<T?> GetByIdAsync(object Id);
        Task InsertAsync(T Entity);
        Task UpdateAsync(T Entity);
        Task DeleteAsync(object Id);
        Task SaveAsync();
    }
}
GenericRepository.cs
using CRUDinCoreMVC.Models;
using Microsoft.EntityFrameworkCore;
namespace CRUDinCoreMVC.GenericRepository
{
    //The following GenericRepository class Implement the IGenericRepository Interface
    public class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        //The following variable is going to hold the EFCoreDbContext instance
        protected readonly EFCoreDbContext _context;

        //The following Variable is going to hold the DbSet Entity
        protected readonly DbSet<T> _dbSet;

        public GenericRepository(EFCoreDbContext context)
        {
            //we are initializing the context object and DbSet variable
            _context = context;

            //Whatever Entity name we specify while creating the instance of GenericRepository
            //That Entity name  will be stored in the DbSet<T> variable
            _dbSet = context.Set<T>();
        }

        //This method will return all the Records from the table
        public async Task<IEnumerable<T>> GetAllAsync()
        {
            return await _dbSet.ToListAsync();
        }

        //This method will return the specified record from the table based on the Primary Key Column
        public async Task<T?> GetByIdAsync(object Id)
        {
            return await _dbSet.FindAsync(Id);
        }

        //This method will Insert one object into the table
        public async Task InsertAsync(T Entity)
        {
            //It will mark the Entity state as Added
            await _dbSet.AddAsync(Entity);
        }

        //This method is going to update an Existing Entity
        public async Task UpdateAsync(T Entity)
        {
            //It will mark the Entity state as Modified
            _dbSet.Update(Entity);
        }

        //This method is going to remove an existing record from the table
        public async Task DeleteAsync(object Id)
        {
            //First, fetch the record from the table
            var entity = await _dbSet.FindAsync(Id);
            if (entity != null)
            {
                //This will mark the Entity State as Deleted
                _dbSet.Remove(entity);
            }
        }

        //This method will make the changes permanent in the database
        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }
    }
}
IEmployeeRepository.cs
using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;

namespace CRUDinCoreMVC.Repository
{
    public interface IEmployeeRepository : IGenericRepository<Employee>
    {
        //Here, you need to define the operations which are specific to Employee Entity

        //This method returns all the Employee entities along with Department data
        Task<IEnumerable<Employee>> GetAllEmployeesAsync();

        //This method returns one the Employee along with Department data based on the Employee Id
        Task<Employee?> GetEmployeeByIdAsync(int EmployeeID);

        //This method will return Employees by Departmentid
        Task<IEnumerable<Employee>> GetEmployeesByDepartmentAsync(int Departmentid);
    }
}

EmployeeRepository.cs
using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;
using Microsoft.EntityFrameworkCore;

namespace CRUDinCoreMVC.Repository
{
    public class EmployeeRepository : GenericRepository<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(EFCoreDbContext context) : base(context) { }

        //Returns all employees from the database including the Department Data
        public async Task<IEnumerable<Employee>> GetAllEmployeesAsync()
        {
            return await _context.Employees.Include(e => e.Department).ToListAsync();
        }

        //Retrieves a single employee by their ID along with Department data.
        public async Task<Employee?> GetEmployeeByIdAsync(int EmployeeID)
        {
            var employee = await _context.Employees
               .Include(e => e.Department)
               .FirstOrDefaultAsync(m => m.EmployeeId == EmployeeID);

            return employee;
        }

        //Retrieves Employees by Departmentid
        public async Task<IEnumerable<Employee>> GetEmployeesByDepartmentAsync(int DepartmentId)
        {
            return await _context.Employees
                .Where(emp => emp.DepartmentId == DepartmentId)
                .Include(e => e.Department).ToListAsync();
        }
    }
}
DepartmentRepository.cs
using CRUDinCoreMVC.GenericRepository;
using CRUDinCoreMVC.Models;

namespace CRUDinCoreMVC.Repository
{
    //Note: if you want to add some methods specific to the Department Entity, you can define here
    public interface IDepartmentRepository : IGenericRepository<Department>
    {
    }

    public class DepartmentRepository : GenericRepository<Department>, IDepartmentRepository
    {
        public DepartmentRepository(EFCoreDbContext context) : base(context) { }
    }
}
Implementing Unit of Work Pattern in ASP.NET Core MVC

Add a folder named UOW within the project root directory. Once the folder is added, add one interface named IUnitOfWork.cs within the UOW folder and copy and paste the following code. As you can see in the code below, we are declaring the operations required to implement the Unit of Work, like Creating the Transaction, Committing the Transaction, Rolling Back the Transaction, and the SaveChanges method, which will make the changes permanent in the database. We also define the specific repositories on which we will be implementing transactions.

using CRUDinCoreMVC.Repository;
namespace CRUDinCoreMVC.UOW
{
    public interface IUnitOfWork
    {
        //Define the Specific Repositories
        EmployeeRepository Employees { get; }
        DepartmentRepository Departments { get; }

        //This Method will Start the database Transaction
        void CreateTransaction();

        //This Method will Commit the database Transaction
        void Commit();

        //This Method will Rollback the database Transaction
        void Rollback();

        //This Method will call the SaveChanges method
        Task Save();
    }
}
Code Explanation:
  • EmployeeRepository Employees { get; }: This property is of type EmployeeRepository and is meant to provide access to employee-related database operations. This repository typically contains methods for adding, updating, deleting, and retrieving employee records.
  • DepartmentRepository Departments { get; }: Similar to the Employees property, this property is of type DepartmentRepository and facilitates access to operations related to the department entities in the database.
  • void CreateTransaction(): This method is intended to start a new database transaction. This sets up a transactional boundary for the operations that follow.
  • void Commit(): This method is designed to commit the transaction that was started with CreateTransaction(). If the operations within the transactional scope are successful, Commit ensures that the changes are saved to the database permanently.
  • void Rollback(): This method is used to roll back or undo all changes made in the current transaction scope if any of the operations fail. This ensures that partial updates do not occur and that the database remains consistent.
  • Task Save(): This asynchronous method calls the SaveChanges method internally to save all changes made in the context of the current transaction to the database. This might be where the changes are actually applied, especially in scenarios where changes are tracked but have not yet persisted.
Implement IUnitOfWork Interface:

Next, we need to implement the Unit of Work interface, which includes methods for starting, committing, and rolling backing transactions. It also tracks changes made to entities during the transaction. Add a class file named UnitOfWork.cs within the UOW folder and copy and paste the following code. The following class will implement the IUnitOfWork interface and provide implementations for the interface members.

using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore;
using CRUDinCoreMVC.Models;
using CRUDinCoreMVC.Repository;

namespace CRUDinCoreMVC.UOW
{
    //Generic UnitOfWork Class
    //Implementing the IUnitOfWork and IDisposable Interfaces
    public class UnitOfWork : IUnitOfWork, IDisposable
    {
        //The following varibale will hold DbContext Instance
        public EFCoreDbContext Context = null;

        //The following varibale will hold the Transaction Instance
        private IDbContextTransaction? _objTran = null;

        //Using the following variable we can call the Operations of GenericRepository and EmployeeRepository
        public EmployeeRepository Employees { get; private set; }

        //Using the following variable we can call the Operations of GenericRepository and DepartmentRepository
        public DepartmentRepository Departments { get; private set; }

        //Initializing the Context, Employees, and Departments objects
        public UnitOfWork(EFCoreDbContext _Context)
        {
            Context = _Context;
            Employees = new EmployeeRepository(Context);
            Departments = new DepartmentRepository(Context);
        }

        //The CreateTransaction() method will create a database Transaction so that we can do database operations
        //by applying do everything and do nothing principle
        public void CreateTransaction()
        {
            //It will Begin the transaction on the underlying connection
            _objTran = Context.Database.BeginTransaction();
        }

        //If all the Transactions are completed successfully then we need to call this Commit() 
        //method to Save the changes permanently in the database
        public void Commit()
        {
            //Commits the underlying store transaction
            _objTran?.Commit();
        }

        //If at least one of the Transaction is Failed then we need to call this Rollback() 
        //method to Rollback the database changes to its previous state
        public void Rollback()
        {
            //Rolls back the underlying store transaction
            _objTran?.Rollback();

            //The Dispose Method will clean up this transaction object and ensures Entity Framework
            //is no longer using that transaction.
            _objTran?.Dispose();
        }

        //The Save() Method will Call the DbContext Class SaveChanges method 
        //So whenever we do a transaction we need to call this Save() method 
        //so that it will make the changes in the database permanently
        public async Task Save()
        {
            try
            {
                //Calling DbContext Class SaveChanges method 
                await Context.SaveChangesAsync();
            }
            catch (DbUpdateException ex)
            {
                // Handle the exception, possibly logging the details
                // The InnerException often contains more specific details
                throw new Exception(ex.Message, ex);
            }
        }

        public void Dispose()
        {
            Context.Dispose();
        }
    }
}
Configuring Dependency Injection for Unit Of Work:
using CRUDinCoreMVC.Models;
using CRUDinCoreMVC.UOW;
using Microsoft.EntityFrameworkCore;

namespace CRUDinCoreMVC
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            //Configure the ConnectionString and DbContext Class
            builder.Services.AddDbContext<EFCoreDbContext>(options =>
            {
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
            });

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            //Registering the UnitOfWork
            builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            //Setting the Employees and Index action method as the default Route
            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Employees}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
Using the Unit of Work in Controllers

Inject IUnitOfWork into your controller. So, modify the Employees Controller as follows:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using CRUDinCoreMVC.Models;
using CRUDinCoreMVC.UOW;

namespace CRUDinCoreMVC.Controllers
{
    public class EmployeesController : Controller
    {
        //The following variable will hold the IUnitOfWork Instance
        private readonly IUnitOfWork _unitOfWork;

        public EmployeesController(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        // GET: Employees
        public async Task<IActionResult> Index()
        {
            //Use Employee Repository to Fetch all the employees along with the Department Data
            var employees = await _unitOfWork.Employees.GetAllEmployeesAsync();

            return View(employees);
        }

        // GET: Employees/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            //Use Employee Repository to Fetch Employees along with the Department Data by Employee Id
            var employee = await _unitOfWork.Employees.GetEmployeeByIdAsync(Convert.ToInt32(id));
            if (employee == null)
            {
                return NotFound();
            }

            return View(employee);
        }

        // GET: Employees/Create
        public async Task<IActionResult> Create()
        {
            ViewData["DepartmentId"] = new SelectList(await _unitOfWork.Departments.GetAllAsync(), "DepartmentId", "Name");
            return View();
        }

        // POST: Employees/Create
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("EmployeeId,Name,Email,Position,DepartmentId")] Employee employee)
        {

            if (ModelState.IsValid)
            {
                try
                {
                    //Begin The Tranaction
                    _unitOfWork.CreateTransaction();

                    //Use Generic Reposiory to Insert a new employee
                    await _unitOfWork.Employees.InsertAsync(employee);

                    //Call SaveAsync to Insert the data into the database
                    //await _repository.SaveAsync();

                    //Save Changes to database
                    await _unitOfWork.Save();

                    //Commit the Changes to database
                    _unitOfWork.Commit();
                    
                    return RedirectToAction(nameof(Index));
                }
                catch (Exception)
                {
                    //Rollback Transaction
                    _unitOfWork.Rollback();
                    //Log The Exception
                }
            }
            ViewData["DepartmentId"] = new SelectList(await _unitOfWork.Departments.GetAllAsync(), "DepartmentId", "Name", employee.DepartmentId);
            return View(employee);
        }

        // GET: Employees/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var employee = await _unitOfWork.Employees.GetByIdAsync(id);
            if (employee == null)
            {
                return NotFound();
            }
            ViewData["DepartmentId"] = new SelectList(await _unitOfWork.Departments.GetAllAsync(), "DepartmentId", "Name", employee.DepartmentId);
            return View(employee);
        }

        // POST: Employees/Edit/5
        // To protect from overposting attacks, enable the specific properties you want to bind to.
        // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("EmployeeId,Name,Email,Position,DepartmentId")] Employee employee)
        {
            if (id != employee.EmployeeId)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    //Begin The Tranaction
                    _unitOfWork.CreateTransaction();

                    //Use Generic Reposiory to Insert a new employee
                    await _unitOfWork.Employees.UpdateAsync(employee);

                    //Save Changes to database
                    await _unitOfWork.Save();

                    //Commit the Changes to database
                    _unitOfWork.Commit();

                    return RedirectToAction(nameof(Index));
                }
                catch (DbUpdateConcurrencyException)
                {
                    //Rollback Transaction
                    _unitOfWork.Rollback();
                }
            }
            ViewData["DepartmentId"] = new SelectList(await _unitOfWork.Departments.GetAllAsync(), "DepartmentId", "Name", employee.DepartmentId);
            return View(employee);
        }

        // GET: Employees/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            //Use Employee Repository to Fetch Employees along with the Department Data by Employee Id
            var employee = await _unitOfWork.Employees.GetEmployeeByIdAsync(Convert.ToInt32(id));

            if (employee == null)
            {
                return NotFound();
            }

            return View(employee);
        }

        // POST: Employees/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            //Begin The Tranaction
            _unitOfWork.CreateTransaction();

            var employee = await _unitOfWork.Employees.GetByIdAsync(id);
            if (employee != null)
            {
                try
                {
                    await _unitOfWork.Employees.DeleteAsync(id);

                    //Save Changes to database
                    await _unitOfWork.Save();

                    //Commit the Changes to database
                    _unitOfWork.Commit();
                }
                catch (Exception)
                {
                    //Rollback Transaction
                    _unitOfWork.Rollback();
                }
            }

            return RedirectToAction(nameof(Index));
        }
    }
}

The IUnitOfWork pattern is particularly useful for operations involving multiple repositories. It ensures that all operations either commit or rollback as a single transaction.

When to Use Unit of Work in ASP.NET Core using EF Core?

Deciding when to use the Unit of Work (UoW) pattern in an ASP.NET Core application that uses Entity Framework Core (EF Core) involves considering the specific needs of your application in terms of transaction management, maintainability, and complexity. Here are some scenarios and considerations that help determine when implementing UoW could be beneficial:

  • Complex Business Transactions: If your application involves complex business processes that require multiple database operations to be executed as part of a single transaction, the UoW pattern is particularly useful. UoW ensures that all these operations either complete successfully or are rolled back together, maintaining data integrity.
  • Consistency Across Multiple Operations: In cases where multiple operations must be performed on different entities but need to remain consistent (i.e., all succeed or all fail), UoW helps manage these operations smoothly. This is often the case in financial systems, e-commerce applications, and other systems where transactional integrity is critical.
  • Aggregating Data Operations: UoW can serve as a central point to manage and commit data operations across multiple repositories. This reduces the dependency on multiple DbContext instances across your application, thus simplifying connection management and improving performance.
  • Testing and Mocking: UoW makes it easier to mock database operations in unit tests. The pattern allows you to abstract transaction committing and focus on the business logic in your tests without touching the database.
  • Reuse of Database Context: The UoW pattern ensures that a single instance of DbContext is used throughout the request lifecycle, which can enhance performance by optimizing connection resources and reducing overhead.
When Not to Use Unit of Work in ASP.NET Core MVC?

While UoW has many benefits, it’s not always necessary or beneficial to use it:

  • Simple CRUD Operations: For applications with simple data access needs that do not require coordinated transactions across multiple operations or repositories, using UoW might be overkill and add unnecessary complexity.
  • Microservices Architecture: In a microservices architecture where each service is meant to be lightweight and independent, implementing a Unit of Work pattern across services can lead to tight coupling and complexity. Each microservice should handle its own data integrity and transaction management internally.
  • When Transactions Are Not Needed: If the operations you perform are non-transactional, or if each operation is independent and does not require transactional consistency with other operations, implementing a Unit of Work pattern could be overkill.
  • Performance Concerns: Although UoW can improve performance by managing connections and contexts efficiently, the abstraction layer it introduces can also lead to a slight performance hit where the operations are straightforward

In the next article, I will discuss How to Upload a File in ASP.NET Core MVC. In this article, I explain how to use the Unit of Work using both Generic and Non-Generic Repositories in the ASP.NET Core MVC Applications using Entity Framework Core. I hope you enjoy this Unit of Work Pattern in the ASP.NET Core MVC Applications using Entity Framework Core article.

Leave a Reply

Your email address will not be published. Required fields are marked *