Skip to main content

Mastering the Unit of Work Pattern in C#: A Complete Guide to Cleaner Database Transactions

 

As a developer, you've likely dealt with a situation where you're working with multiple database operations. Maybe you're creating an item, updating another, and deleting something else all within the same business transaction. But here’s the kicker: if one of these operations fails, you need to undo the others. Managing these complex interactions can become messy fast.

That’s where the Unit of Work (UoW) pattern comes to the rescue. This pattern is your toolkit for managing changes to the database in a clean, reliable, and maintainable way. Ready to dive into how this can clean up your codebase? Let’s break it down.

What is the Unit of Work Pattern? πŸ”§

At its core, the Unit of Work pattern is about treating a series of database operations as a single transaction. Imagine you're working with an e-commerce app, and you need to:

  1. Add a new item to your inventory.
  2. Update stock levels for related products.
  3. Log this transaction for auditing purposes.

Now, if any of these steps fail—like a stock level update throwing an error—you don’t want to leave the database in an inconsistent state. You need a way to roll back all the changes made in that transaction.

The Unit of Work pattern groups all these operations together, ensuring they are either all successful or none at all. It’s essentially like saying, “If one thing breaks, undo everything and keep my database clean.”

How Does It Work in .NET Core?

In .NET Core, the Unit of Work pattern pairs beautifully with Entity Framework (EF), which already provides some built-in capabilities to manage transactions. But if you want more control or are using repositories for your data access, the Unit of Work pattern offers a clear structure for managing database operations.

Let’s dive into a real-world example with an Item API.

The Problem: Managing Multiple Repositories

Imagine you're building an API for managing items in a store’s inventory. You might have repositories like:

  • ItemRepository: Manages the item data (CRUD operations).
  • StockRepository: Handles stock levels for each item.
  • AuditRepository: Logs every transaction for future reference.

If you're not using Unit of Work, you might call these repositories separately in your service class like so:

public class ItemService
{
    private readonly IItemRepository _itemRepository;
    private readonly IStockRepository _stockRepository;
    private readonly IAuditRepository _auditRepository;

    public ItemService(IItemRepository itemRepository, IStockRepository stockRepository, IAuditRepository auditRepository)
    {
        _itemRepository = itemRepository;
        _stockRepository = stockRepository;
        _auditRepository = auditRepository;
    }

    public void AddNewItem(Item item)
    {
        _itemRepository.Add(item);
        _stockRepository.UpdateStock(item);
        _auditRepository.LogTransaction(item);
    }
}
Now, imagine that the stock update fails halfway through. The item gets added to the database, but the stock levels are off, and no audit record is logged. Yikes! Without UoW, you're left to manually handle rolling back changes, which gets tricky and error-prone.

Enter the Unit of Work: Clean, Reliable Transactions

Let’s bring in the Unit of Work pattern to manage all these operations in a single transaction. Here's how we do it:

  1. Create a Unit of Work Interface: This will serve as an abstraction for all your repositories and ensure they work together in a transaction.
  2. Implement the Unit of Work Class: This class will handle saving changes and managing the lifecycle of your repositories.

Step 1: Define the Unit of Work Interface

public interface IUnitOfWork : IDisposable
{
    IItemRepository ItemRepository { get; }
    IStockRepository StockRepository { get; }
    IAuditRepository AuditRepository { get; }
    void Commit();
}

Step 2: Implement the Unit of Work Class

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;

    public IItemRepository ItemRepository { get; private set; }
    public IStockRepository StockRepository { get; private set; }
    public IAuditRepository AuditRepository { get; private set; }

    public UnitOfWork(ApplicationDbContext context, IItemRepository itemRepository, IStockRepository stockRepository, IAuditRepository auditRepository)
    {
        _context = context;
        ItemRepository = itemRepository;
        StockRepository = stockRepository;
        AuditRepository = auditRepository;
    }

    public void Commit()
    {
        _context.SaveChanges();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Step 3: Refactor the Service Class

Now, instead of interacting with each repository directly, the ItemService can call the Unit of Work:

public class ItemService
{
    private readonly IUnitOfWork _unitOfWork;

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

    public void AddNewItem(Item item)
    {
        _unitOfWork.ItemRepository.Add(item);
        _unitOfWork.StockRepository.UpdateStock(item);
        _unitOfWork.AuditRepository.LogTransaction(item);
        _unitOfWork.Commit(); // All operations succeed or fail together
    }
}
Now, when you call AddNewItem, it ensures that the item, stock, and audit log are all committed in a single database transaction. If something fails, nothing is saved.

Why Use Unit of Work? πŸ€”

  1. Atomic Operations: With Unit of Work, you ensure that a group of operations is treated as a single unit. Either everything succeeds, or nothing does.

  2. Improved Maintainability: Centralizing your transaction logic into a single place (the Unit of Work) keeps your service classes clean and focused on business logic.

  3. Reduced Redundancy: You avoid writing boilerplate code for transaction management across multiple services.

But What About Performance?

You might be wondering: Doesn't Unit of Work slow things down?

The short answer is no—at least, not in a way that outweighs its benefits. The overhead introduced by Unit of Work is minimal, and in most cases, it actually improves performance by reducing unnecessary database roundtrips and managing state more effectively.

Wrapping It All Up

The Unit of Work pattern is like the glue that holds your repositories together in a single, reliable transaction. It makes your code cleaner, easier to maintain, and ensures that your database stays consistent even when things go wrong. So next time you're working with multiple repositories or complex operations in your .NET Core project, give the Unit of Work pattern a shot—it might just save your code (and your sanity).

Your turn! How have you managed complex transactions in your own projects? Have you run into issues with inconsistent data before? Share your thoughts and experiences in the comments below!

Comments

Popular posts from this blog

Optional Parameters in C# — Writing Flexible and Clean Methods

Hello, .NET developers! πŸ‘‹ How often have you created multiple method overloads just to handle slightly different cases? Maybe one method accepts two parameters, another three, and one more adds a flag for debugging? That’s a lot of code duplication for something that can be solved beautifully with optional parameters . Optional parameters in C# let you define default values for method arguments. When a caller doesn’t pass a value, the compiler automatically substitutes the default. This feature helps keep your APIs simple, readable, and maintainable. πŸŽ₯ Explore more on YouTube : DotNet Full Stack Dev Understanding Optional Parameters Optional parameters are defined by assigning default values in the method signature. When calling the method, you can omit those parameters if you’re okay with the defaults. Example public class Logger { public void Log(string message, string level = "INFO", bool writeToFile = false) ...

.NET 10: Your Ultimate Guide to the Coolest New Features (with Real-World Goodies!)

 Hey .NET warriors! πŸ€“ Are you ready to explore the latest and greatest features that .NET 10 and C# 14 bring to the table? Whether you're a seasoned developer or just starting out, this guide will show you how .NET 10 makes your apps faster, safer, and more productive — with real-world examples to boot! So grab your coffee ☕️ and let’s dive into the awesome . πŸ’ͺ 1️⃣ JIT Compiler Superpowers — Lightning-Fast Apps .NET 10 is all about speed . The Just-In-Time (JIT) compiler has been turbocharged with: Stack Allocation for Small Arrays πŸ—‚️ Think fewer heap allocations, less garbage collection, and blazing-fast performance . Better Code Layout πŸ”₯ Hot code paths are now smarter, meaning faster method calls and fewer CPU cache misses. πŸ’‘ Why you care: Your APIs, desktop apps, and services now respond quicker — giving users a snappy experience . 2️⃣ Say Hello to C# 14 — More Power in Your Syntax .NET 10 ships with C# 14 , and it’s packed with developer goodies: Field-Bac...

Implementing and Integrating RabbitMQ in .NET Core Application: Shopping Cart and Order API

RabbitMQ is a robust message broker that enables communication between services in a decoupled, reliable manner. In this guide, we’ll implement RabbitMQ in a .NET Core application to connect two microservices: Shopping Cart API (Producer) and Order API (Consumer). 1. Prerequisites Install RabbitMQ locally or on a server. Default Management UI: http://localhost:15672 Default Credentials: guest/guest Install the RabbitMQ.Client package for .NET: dotnet add package RabbitMQ.Client 2. Architecture Overview Shopping Cart API (Producer): Sends a message when a user places an order. RabbitMQ : Acts as the broker to hold the message. Order API (Consumer): Receives the message and processes the order. 3. RabbitMQ Producer: Shopping Cart API Step 1: Install RabbitMQ.Client Ensure the RabbitMQ client library is installed: dotnet add package RabbitMQ.Client Step 2: Create the Producer Service Add a RabbitMQProducer class to send messages. RabbitMQProducer.cs : using RabbitMQ.Client; usin...