2025-01-14

A unit of work keeps track of the object representation of a transaction. In other words, it manages a registry of what objects should be created, updated, and deleted. It allows us to combine multiple changes in a single transaction (one database call), offering multiple advantages over calling the database every time we make a change.Assuming we are using a relational database, here are two advantages:

  • First, it can speed up data access; calling a database is slow, so limiting the number of calls and connections can improve performance.
  • Second, running a transaction instead of individual operations allows us to roll back all operations if one fails or commit the transaction as a whole if everything succeeds.

EF Core implements this pattern with the DbContext class and its underlying types, such as the DatabaseFacade and ChangeTracker classes.Our small applications don’t need transactions, but the concept remains the same. Here is an example of what happens using EF Core:

var product = _db.Products.Find(productId);
product.QuantityInStock += amount;
_db.SaveChanges();

The preceding code does the following:

  1. Queries the database for a single entity.
  2. Changes the value of the QuantityInStock property.
  3. Persists the changes back into the database.

In reality, what happened is closer to the following:

  1. We ask EF Core for a single entity through the ProductContext (a unit of work), which exposes the DbSet property (the product repository). Under the hood, EF Core does the following:
    A. Queries the database.
    B. Caches the entity.
    C. Tracks changes for that entity.
    D. Returns it to us.
  2. We change the value of the QuantityInStock property; EF Core detects the change and marks the object as dirty.
  3. We tell the unit of work to persist the changes that it tracked, saving the dirty product back to the database.

In a more complex scenario, we could have written the following code:

_db.Products.Add(newProduct);
_db.Products.Remove(productToDelete);
product.Name = “New product name”;
_db.SaveChanges();

Here, the SaveChanges() method triggers saving the three operations instead of sending them individually. You can control database transactions using the Database property of DbContext (see the Further reading section for more information).Now that we’ve explored the unit of work pattern, we could implement one by ourselves. Would that add value to our application? Probably not. If you want to build a custom unit of work or a wrapper over EF Core, there are plenty of existing resources to guide you. Unless you want to experiment or need a custom unit of work and repository (which is possible), I recommend staying away from doing that. Remember: do only what needs to be done for your program to be correct.

Don’t get me wrong when I say do only what needs to be done; wild engineering endeavors and experimentations are a great way to explore, and I encourage you to do so. However, I recommend doing so in parallel so that you can innovate, learn, and possibly even migrate that knowledge to your application later instead of wasting time and breaking things. If you are using Git, creating an experimental branch is a good way of doing this. You can then delete it when your experimentation does not work, merge the branch if it yields positive results, or leave it there as a reference (depending on the team’s policies in place).

Now that we explored a high-level view of the Repository and Unit of Work patterns, and what those common layers are for, we can continue our journey of using layers.

Leave a Reply

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