How to Break Out Aggregate Roots and Repositories in a Legacy Entity Framework Application

I started working at my current job about a year ago. They are a bright group, filled with a diverse set of development talent. The applications our team works on are multi-tiered with extremely complex business logic, a DDD enthusiast’s heaven. However, one of the first things I noticed when I got on board with this team was that the data access layer classes were non-standardized, and contained some business logic.

Most of us by now have seen the Repository Pattern in practice, or have at least read or watched a video on it and its usefulness in and outside of Domain Driven Design. Most of the situations in these videos are contrived in a sense that the developers are starting a project from scratch, and have the option to bake in the Entity Framework into the actual core domain objects using EF Code First. This approach is elegant and definitely cuts down on the amount of code required to persist domain objects in repositories, but implementing it in legacy applications requires quite a bit of changes, both in the database and the client application’s code. The approach we took was to keep our current EF classes intact, continue to use them as part of the repository’s implementation details, and redefine the core domain objects coming out of the repositories.

Up until recently, it would have been pretty common to see a data access layer and core model classes in our Entity Framework application defined similarly to the following:

// Defines the inventory model class.
public class Inventory
{
    // Gets or sets the identifier.
    public int Id { get; set; }

    // Gets or sets the collection of inventory items.
    public List<InventoryItems> Items { get; set;}
}

// Defines the inventory item model class.
public class InventoryItem
{
    // Gets or sets the identifier.
    public int Id { get; set; }

    // Gets or sets the price.
    public float Price { get; set; }
}

// Contains the data access layer methods for inventory related operations.
public static class InventoryDal
{
    // Gets all the items for the specified inventory identifier.
    public static InventoryItem[] GetAllItemsForInventory(int id)
    {
        using (var context = new DbContext())
        {
            var dbInventoryItems = context.DbInventories.Find(id).InventoryItems.ToArray();
            return CreateInventoryItemsFromDbInventoryItems(dbInventoryItems);
        }
    }

    // Updates the price for the specified inventory item.
    public static bool UpdateItemPriceForInventory(InventoryItem item)
    {
        if (item.Price <= 0)
        {
            return false;
        }

        using (var context = new DbContext())
        {
            var dbInventoryItem = context.DbInventoryItems.Find(item.Id);
            dbInventoryItem.Price = item.Price;
            return context.SaveChanges() > 0;
        }
    }

    // and many other inventory and inventory item related operations
}

Numerous improvements can be made to these classes. Most notably in the InventoryDal class, the static methods prevent injection and inhibit unit-testing the client classes utilizing it. And since the domain logic for performing a price update is in the InventoryDal class, it is highly unlikely to be unit tested itself. Also note there are aggregate root boundary violations, as the InventoryDal class exposes methods to query for InventoryItem objects without accessing them through an Inventory aggregate root.

As for the model classes, they are anemic models that are simply mapped from data in tables out of a relational database. The identifiers, along with other properties, can be changed at will, by any client code. Because of this, both an Inventory and an InventoryItem objects are potentially in an invalid state. Because the aggregate is not well bounded, its invariants cannot be enforced.

Clearer Inventory and InventoryItem core models coupled with a standard repository could address several of these issues. Let’s take a look at how the model should look, and define an aggregate root.

It’s clear from the two visible methods in the InventoryDal class, the application can manage a collection of inventories, potentially for separate stores. Each inventory contains a collection of inventory items, which in turn have a price. Given this, I would expect a basic model to look something like this:

// Defines the inventory domain model.
public class Inventory
{
    private int id;
    private List<InventoryItem> items = new List<InventoryItem>();

    // Initializes a new instance of the Inventory class.
    public Inventory(int id)
    {
        this.id = id;
    }

    // Adds the specified item to the inventory.
    public void AddInventoryItem(InventoryItem item)
    {
        items.Add(item);
    }

    // Removes the specified item from the inventory.
    public void RemoveInventoryItem(InventoryItem item)
    {
        items.Remove(item);
    }

    // Gets the identifier of this inventory object.
    public int Id => id;

    // Gets the collection of items associated with this inventory object.
    public IEnumerable Items => items.ToArray();
}

// Defines the inventory item domain model.
public class InventoryItem
{
    private int id;
    private float price;

    // Initializes a new instance of the InventoryItem class.
    public InventoryItem(int id, float price)
    {
        this.id = id;
        this.price = Math.Max(price, 0);
    }

    // Changes the price of the inventory item to the specified value.
    public bool ChangePrice(float price)
    {
        if (price <= 0)
        {
            return false;
        }

        this.price = price;
    }

    // Gets the identifier of this inventory item object.
    public int Id => id;

    // Gets the price of the inventory item.
    public float Price => price;
}

This defines the Inventory class as the aggregate root. Since an InventoryItem cannot exist without the context of an Inventory, the InventoryItem class cannot be an aggregate root. The InventoryItem class is simply an entity that will be managed inside the Inventory aggregate root boundary.

Note that the domain logic for updating the price moved into the InventoryItem class. Moving the logic isolates it into a rich domain model, allowing it to be unit tested. It also allows clients performing a price update to know if the price update will satisfy the domain rules before attempting to persist it to the persistence layer. An additional benefit of this design is that an InventoryItem object will never be in a state in which it has an invalid price; as part of the Inventory aggregate root, it helps maintains the validity of the aggregate on a price update.

Taking this model, we can now define a standard repository interface for our aggregate root.

// Defines an interface for an inventory repository.
public interface IInventoryRepository
{
    // Gets the inventory with the specified identifier.
    Inventory GetInventory(int id);

    // Gets all the inventory objects in the repository.
    IEnumerable<Inventory> GetInventories();

    // Adds the specified inventory object to the repository.
    bool AddInventory(Inventory inventory);

    // Removes the specified inventory object from the repository.
    bool RemoveInventory(Inventory inventory);

    // Updates the repository with the specified inventory object.
    bool UpdateInventory(Inventory inventory);
}

Because Inventory is the aggregate root, the repository only operates on objects of the Inventory type. Thus, clients will have to go through an Inventory aggregate root to update its InventoryItem objects. This decision inherently simplifies our access to the persistence layer. The implementation of the repository would look something like the following:

// Provides an implementation of the IInventoryRepository interface.
public class InventoryRepository : IInventoryRepository
{
    // Gets the inventory with the specified identifier.
    public Inventory GetInventory(int id)
    {
        using (var context = new DbContext())
        {
            var dbInventory = dbContext.DbInventories.Find(id);
            return this.CreateInventoryFromDbInventory(dbInventory);
        }
    }

    // Gets all the inventory objects in the repository.
    public IEnumerable<Inventory> GetInventories()
    {
        using (var context = new DbContext())
        {
            var dbInventories = dbContext.DbInventories.ToArray();
            return this.CreateInventoriesFromDbInventories(dbInventories);
        }
    }

    // Adds the specified inventory object to the repository.
    public bool AddInventory(Inventory inventory)
    {
        using (var context = new DbContext())
        {
            var dbInventory = this.CreateDbInventoryFromInventory(inventory);
            context.DbInventories.Add(dbInventory);
            return context.SaveChanges() > 0;
        }
    }

    // Removes the specified inventory object from the repository.
    public bool RemoveInventory(Inventory inventory)
    {
        using (var context = new DbContext())
        {
            var dbInventory = context.DbInventories.Find(inventory.Id);
            context.DbInventories.Remove(dbInventory);
            return context.SaveChanges() > 0;
        }
    }

    // Updates the repository with the specified inventory object.
    public bool UpdateInventory(Inventory inventory)
    {
        using (var context = new DbContext())
        {
            var dbInventory = context.DbInventories.Find(inventory.Id);
            this.UpdateDbInventoryFromInventory(dbInventory, inventory);
            return context.SaveChanges() > 0;
        }
    }

    // Some other factory methods used for creating and updating DbInventory and Inventory objects.
}

If we look at how a price update was performed with the InventoryDal implementation, the Inventory class wasn’t even needed to do a price update. This decision, coupled with having the domain logic in the InventoryDal class prevented both the Inventory and InventoryItem classes from being able to enforce any invariants required by that domain operation.

var items = InventoryDal.GetAllItemsForInventory(inventoryId);
var item = items.FirstOrDefault(i => i.Id == itemId);
item.Price = newPrice;
var success = InventoryDal.UpdateItemPriceForInventory(item);

Looking at the refactored implementation, the price update is forced through the Inventory and InventoryItem objects, allowing clients to determine if the price update will succeed before attempting to persist the change.

var inventory = repository.GetInventory(inventoryId);
var item = inventory.GetInventoryItem(itemId);
var success = item.ChangePrice(newPrice);
if (success)
{
    success = repository.UpdateInventory(inventory);
}

In closing, defining aggregate root boundaries is the first step in bringing a legacy EF application up to speed with DDD concepts. Once the core models have been defined, and domain logic restructured, a repository can be stood up in place of an antiquated DAL. The rest of the application’s refactor can then fall into place with the new core objects and repositories to use.

For further reading on Domain Driven Design, aggregate roots and the repository pattern, check out Eric Evan’s website at domainlanguage.com. I’d also recommend checking out some of Julie Lerman’s articles on DDD with the Entity Framework.

Advertisements