NashTech Blog

Unit of Work: Redundant or Absolutely Essential? Debunking the .NET Architectural Debate

Table of Contents


Unit of Work: Redundant or Absolutely Essential? Debunking the .NET Architectural Debate

Hello everyone, I’m Hoang, the owner of CleanBase – a .NET package with nearly 20k downloads on NuGet Gallery | hoangvh238

CleanBase helps you write clean, Clean Architecture-compliant code, and the Unit of Work (UoW) and Repository patterns are the indispensable backbone. In this article, we’ll dissect the biggest debate in the .NET community: Is the Unit of Work pattern still necessary when DI already handles DbContext sharing?

1. Introduction: The Clash Between Technique and Architecture

The Shocking Confession

I have this friend, born in ’96, studied Mechanical Engineering at Bach Khoa. He once said to me:

“Hoang, I think Unit of Work is obsolete! With modern EF Core and DI, I just inject a shared DbContext into my Repositories and everything commits automatically!”

I laughed and said, “Well, technically… you’re half-right =)))”

The truth is, Scoped DI in .NET ensures that all Repositories within the same HTTP request share the same DbContext instance. That means if you call _context.SaveChangesAsync() anywhere in that scope, all changes are committed.

So then… why would we still bother adding a UnitOfWork class?

The Core Issue: Violation of the Single Responsibility Principle (SRP)

Relying solely on this technical shortcut to manage transactions is a serious architectural flaw. It violates the Single Responsibility Principle (SRP) by mixing the responsibilities of Data Access and Transaction Management within the same layer.

As Robert C. Martin (Uncle Bob) famously said:

“The first rule of clean code is simplicity. The second rule of clean code is abstraction.”

You are forcing the Service Layer or the Repository to bear the burden of Transaction Management (a persistence concern) instead of focusing solely on Business Logic. This is precisely where UoW steps in.


2. Separation of Concerns: Repository vs. Unit of Work

In a clean architecture, each pattern has a clear, non-overlapping responsibility:

A. Repository: Specializing in Data Access

The Repository’s sole responsibility is to query, add, update, and delete Entities, it marks the changes. A Repository should never decide when those changes are saved to the database.

  • Rule: The Repository only calls methods like _dbSet.AddAsync(entity) or _dbSet.Remove(entity).
  • The Flaw: If the Repository calls SaveChanges, it breaks the atomicity of larger Business Transactions that involve multiple entities across different Repos.

B. Unit of Work: The Transaction Gatekeeper

The Unit of Work is the single layer of abstraction responsible for deciding when to Commit or Rollback a sequence of marked changes.

LayerIdeal Responsibility (SRP)Role of UoW
RepositoryQuery/Modify Entities.Provides unsaved operations.
Service LayerContains Business Logic.Delegates responsibility for transaction management.
Unit of WorkManages the lifecycle and Commits the entire transaction.Ensures Atomicity (all or nothing).

3. Standard Implementation: Transparency and Testability

To make UoW function as a perfect abstraction, we must use Constructor Injection for all dependencies.

A. UoW and Constructor Injection

The UoW receives its dependencies (DbContext and all Repositories) via its constructor. This ensures transparency of dependencies and atomicity, as all components share the same Scoped DbContext instance.

C#

public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;

// UoW receives DbContext and Repositories via Constructor Injection
public UnitOfWork(DbContext context, IUserRepository userRepository, IOrderRepository orderRepository)
{
_context = context;
UserRepository = userRepository;
OrderRepository = orderRepository;
}

// ...
public async Task<int> CompleteAsync()
{
// **SOLE RESPONSIBILITY:** Execute the atomic transaction
return await _context.SaveChangesAsync();
}
}

B. Service Layer: Where Abstraction Shines

The Service Layer depends only on IUnitOfWork (a pure interface), not on the concrete DbContext class.

C#

public class UserService
{
    private readonly IUnitOfWork _unitOfWork;

    // Depends on IUnitOfWork, NOT DbContext
    public UserService(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; }

    public async Task CreateUserAndOrderAsync(UserDto userDto, OrderDto orderDto)
    {
        // Perform operations via UoW (marking changes)
        await _unitOfWork.UserRepository.AddAsync(user);
        await _unitOfWork.OrderRepository.AddAsync(order);

        // Delegate responsibility: Request UoW to finalize the transaction
        await _unitOfWork.CompleteAsync(); 
    }
}

The Greatest Benefit:

  • Testability: When testing UserService, you only need to Mock IUnitOfWork. The business logic becomes completely decoupled from the data layer.
  • Extensibility: If you swap the ORM (e.g., from EF Core to Dapper), you only rewrite the UoW implementation, while the Service Layer remains untouched.

4. Conclusion: Choosing Architectural Integrity

The argument that “DI is enough” is a perfect example of prioritizing technical convenience over architectural integrity.

The Unit of Work pattern is essential because:

  1. It enforces the Single Responsibility Principle (SRP) rigorously.
  2. It provides the Abstraction necessary to shield the Service Layer from concrete ORM technology.
  3. It maintains absolute Data Integrity by managing atomic transactions.

Build your application with Architectural Steel, where every layer adheres to its prescribed responsibility. Feel free to explore and use CleanBase to see the Unit of Work pattern implemented effectively.

Picture of Hoang Vo Huy

Hoang Vo Huy

Leave a Comment

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

Suggested Article

Scroll to Top