Functional DDD with C# Part 5: entities and aggregates

Functional DDD with C# Part 5: entities and aggregates

Introduction

Now that you know a lot about the functional representation of value objects, it is time to switch to entities and aggregates.

Entities in a few words

Unlike value objects, entities are data structures that have an identity. Let's take an example: a shopping cart. It must be traced in the purchase flows, so it must be assigned an identity.

Let’s start from this very first definition, we will improve it throughout this article:

public class Cart
{
    public int Id { get; set; } // An entity have an identifier
    public int TotalAmountWithoutTax { get; set; }
    // Incoming : Cart Items
}

The composition of an entity

An entity should consist of:

  • Value objects (or list of value objects).

  • And/or entities (or list of entities).

Let's see why.

A word on primitive obsession first

In the current cart implementation, we used two integers for the identifier and the total amount. Using primitives is a bad idea because it leads to the writing of defensive code, that is, code that does not believe in itself. This is called "Primitive obsession".

For example, suppose we need to calculate the total cart price with tax. What's more, the business asks us to implement two validations:

  • Tax amount cannot be negative.

  • The total amount of an order cannot exceed 10000.

It may be tempting to write this:

public class Cart 
{
    public int Id { get; set; }
    public int TotalAmountWithoutTax { get; set; }
    public int TotalAmountWithTax { get; set; }
    // Incoming : Cart Items

    public void SetTotalAmountWithTax(int taxAmount) 
    {
        if (taxAmount < 0) 
            throw new BusinessException("The tax amount cannot be negative.");

        var totalAmountWithTax = TotalAmountWithoutTax * taxAmount;        
        if (totalAmountWithTax > 10000) 
            throw new BusinessException("The total amount cannot be more than 10000.");

        TotalAmountWithTax = totalAmountWithTax;
    }
}

The problem is that this code is very defensive: several validations are carried out before executing the code. Now imagine these rules may have to be implemented in other places in the code: there is a high risk of code duplication.

An entity should consist of value objects...

Now we have seen value objects are so important, we will systematically use value objects in our entities instead of using primitives. They will our elementary blocks of code. We will define two of them: Amount and Identifier.

Here is the Amount Value Object. It is very important to define them as record so they remain immutable.

public record Amount
{
    private const int MAX_AMOUNT = 10000;
    private const int MIN_AMOUNT = 0;

    public readonly int Value;

    public Amount(int amount) 
    {
        if (amount < MIN_AMOUNT) 
            throw new BusinessException("An amount cannot be negative.");
        if (amount > MAX_AMOUNT)
            throw new BusinessException($"An amount cannot be more than {MAX_AMOUNT}.");

        Value = amount;
    }
}

Our cart class is slimmed down and now looks like this:

public class Cart 
{
    public Identifier Id { get; set; }
    public Amount TotalAmountWithoutTax { get; set; }
    // Incoming : Cart Items, Client

    public Amount GetTotalAmountWithTax(Amount taxAmount) 
    {
        return new Amount(TotalAmountWithoutTax.Value * taxAmount.Value);        
    }
}

With Value Objects, you no longer need elementary checks or even to think about it. I recommend that you apply a strict rule: do not use any primitive in the entities. It is restrictive at first, but after that, it will ease your mind.

The only downside of Value Objects is the lack of context in the error messages. For example, you will get a standard error "An amount cannot be negative." But I think that's OK because we usually know the context when the error occurs. If you really need to contextualize the error message, you can assign a code to the error, then catch that code and change the error message where you need to.

... and they should consist of other entities

An entity should consist of other entities or a collection of entities. In that case, it becomes an aggregate and it must be saved as a whole in the database.

For example, our cart becomes an aggregate when we introduce the items:

public class Cart 
{
    public Identifier Id { get; set; }
    public ImmutableList<Item> items { get; set; } 
    public Amount TotalAmountWithoutTax { get; set; }
    public Amount TotalAmountWithTax { get; set; }

    public void SetTotalAmountWithTax(Amount taxAmount) { /* ... */ }
}

In an aggregate, one entity must be designated as the aggregate root: it is responsible for the consistency of all the data in the aggregate. For example, the operation of updating a child entity is the responsibility of the aggregate root.

In our example, the aggregate root is the cart entity and it will embed the modification of an item. Thus, the total amount of the basket will be updated and will remain consistent with the current items in the basket.

public class Cart 
{
    public Identifier Id { get; set; }
    public List<Item> items { get; set; } 
    public Amount TotalAmountWithoutTax { get; set; }
    public Amount TotalAmountWithTax { get; set; }

    public void SetTotalAmountWithTax(Amount taxAmount) { /* ... */ }

    // Updating the quantity of an item is the responsability of the cart class
    public void UpdateItem(Item item){ /* ... */ }
}

Note that it is very important to favor simple identifiers instead of entities to limit the size of the aggregates. Do not try to model the reality: use only the data you need to persist the aggregate and maintain its consistency.

For more information about DDD, aggregates, entities and value objects in .Net, I strongly recommend the lecture on "Patterns, principles and practices of DDD" where the concepts are extremely well explained.

A functional entity

A read-only data structure

As we have seen previously, data structures are immutable in functional programming. The easiest change to make is transforming the class into a record like the value objects, and transforming the Lists into immutable lists.

public record Cart 
{
    public Identifier Id { get; init; }
    public ImmutableList<Item> Items { get; init; }
    public Amount TotalAmountWithoutTax { get; init; }
    public Amount TotalAmountWithTax { get; init; }
}

Behaviors

In functional programming, data is immutable and there is no state. Thus, behaviors become static extension methods of the immutable data structure and they return a new instance of it:

public static class CartExtensions 
{
    public static Cart SetTotalAmountWithTax(this Cart cart, Amount taxAmount) 
    {
        return cart with { TotalAmountWithTax = new Amount(cart.TotalAmountWithoutTax.Value * taxAmount.Value) } ;
    }
}

An Entity<T> container

As we have seen in previous articles, containers are a great help for chaining function calls. This will serve us a lot in the Application layer we will study it in the next article.

An Entity<T> container has been defined in my little library Grenat.Functional.DDD. Like ValueObject<T> it has two states: one valid and one invalid. Bind and Match operations have also been defined. See the previous article of this series for more details.

The challenges of immutability and container composition

Dealing with immutability and containerized entities is not an easy task. We will face several problems. Let's see why and how to tackle them.

The problem of container composition

If you read my previous article, Value Objects will be created and containerized in a ValueObject<T> container thanks to their static constructor. And they will be themselves containerized in a parent Entity<T> container. And we face the problem of a container containing other containers, i.e. an Entity<T> containing some ValueObject<T> or some Entity<T>. This is not convenient.

We don't want to write this:

public record Cart
{
    public ValueObject<Identifier> Id { get; init; }
    public ImmutableList<Entity<Item>> Items { get; init; }
    public ValueObject<Amount> TotalAmountWithoutTax { get; init; }
    public ValueObject<Amount> TotalAmountWithTax { get; init; }

    /*...*/
}

We want to write this:

public record Cart
{
    public Identifier Id { get; init; }
    public ImmutableList<Item> Items { get; init; }
    public Amount TotalAmountWithoutTax { get; init; }
    public Amount TotalAmountWithTax { get; init; }

    /*...*/
}

What's more, whenever we need to change an entity's property, we have to recreate a new instance of it to stick with immutability principles.

To solve these problems, Grenat.Functional.DDD contains some setters:

  • SetValueObject

  • SetEntity

  • SetEntityList

  • SetEntityDictionary

  • SetEntityOption

  • SetValueObjectOption

Behind the scenes, these setters :

  1. Unwrap the inner value of the Entity<T> to modify. Then :

  2. Unwrap the inner value of the container (an Entity<V>, a ValueObject<V>, an Option<V>, ...) that is passed as a parameter,

  3. They modify the targeted property of Entity<T> thanks the setter function,

  4. And they wrap the result in an new instance of Entity<T>.

If one of the Entity or ValueObject parameter is invalid, then the resulting Entity<T> will become invalid too.

Finally, Errors are harvested into this resulting entity.

Let's study them.

SetValueObject

Here is the function's signature :

public static Entity<T> SetValueObject<T, V>(this Entity<T> entity, ValueObject<V> valueObject, Func<T, V, T> setter) { /*...*/ }

It takes as arguments:

  • The entity.

  • The value object to set into the entity.

  • A setter function.

You can use it this way:

public static Entity<Cart> SetTotalAmount(this Entity<Cart> cart, ValueObject<Amount> totalAmount)
{
     return cart.SetValueObject(totalAmount, static (cart, totalAmount) => cart with { TotalAmount = totalAmount });
}

SetEntity

SetEntity is the same as SetValueObject but instead of accepting a ValueObject<T> as a parameter, it accepts an Entity<T>. Here is its signature:

public static Entity<T> SetEntity<T, E>(this Entity<T> parentEntity, Entity<E> entity, Func<T, E, T> setter) { /* ... */ }

SetValueObjectList, SetEntityList, SetEntityDictionary, SetEntityOption...

As their name suggests, these setters do the same than the previous ones, but for immutable lists, immutable dictionaries and optionable entities and value objects.

Error harvesting

All setters perform error harvesting. That is to say, if you try to set an invalid value object or an invalid entity, their errors are harvested and added to the ones already existing on the parent entity. It is very interesting for APIs: if the user types bad data, then all the errors will be returned.

Let's have a look at an example, and let's define two value objects Identifier and Amount, written following the guidelines of the previous article:

Here is an Identifier value object:

public record Identifier
{
    public const string DEFAULT_VALUE = "";
    public readonly string Value = "";
      
    public Identifier() { Value = DEFAULT_VALUE; }

    private Identifier(string identifier)
    {
        Value = identifier;
    }

    public static ValueObject<Identifier> Create(string identifier)
    {
        if (string.IsNullOrEmpty(identifier))
            return new Error("An identifier cannot be null or empty.");

        return new Identifier(identifier);
    }
}

And here is an Amount value object:

public record Amount
{
    public const int MAX_AMOUNT = 2500;
    public const int DEFAULT_VALUE = 0;
    public const string DEFAULT_CURRENCY = "EUR";

    public readonly int Value;
    public readonly string Currency = DEFAULT_CURRENCY;

    public Amount() { Value = DEFAULT_VALUE; }

    private Amount(int value, string currency = DEFAULT_CURRENCY)
    {
        Value = value;
        Currency = currency;
    }

    public static ValueObject<Amount> Create(int amount)
    {
        if (amount < 0)
            return new Error("An amount cannot be negative.");
        if (amount > MAX_AMOUNT)
            return new Error(String.Format($"An amount cannot be more than {MAX_AMOUNT}"));

        return new Amount(amount);
    }
}

Now let's create a cart by setting invalid data and let's see the errors harvesting in action. Cart.Create() is a static constructor returning an empty Entity<Cart>:

var cart = Cart.Create()
            .SetValueObject(Identifier.Create(null), static (cart, identifier) => cart with { Id = identifier })
            .SetValueObject(Amount.Create(-1), static (cart, totalAmountWithoutTax) => cart with { TotalAmountWithoutTax = totalAmountWithoutTax })
            .SetValueObject(Amount.Create(3000), static (cart, totalAmountWithTax) => cart with { TotalAmountWithTax = totalAmountWithTax });

Console.WriteLine(cart.Errors);
// Output :
// An identifier cannot be null or empty.
// An amount cannot be negative.
// An amount cannot be more than 2500.

But, I agree with you, this code is a bit cryptic and not very "ubiquitous language"... Can we do better? Of course we can! Let's have a look.

Creating "ubiquitous language" setters

For better readability, you can create some setters using extension methods. They will call SetValueObject, SetEntity, SetEntityList, SetEntityDictionary functions under the hood. But be careful, Setters can be an anti-pattern that leads to data inconsistency, so use them wisely.

Here is an example:

public static class CartSetters
{
    public static Entity<Cart> SetId(this Entity<Cart> cart, string id) 
    {
        return cart.SetValueObject(Identifier.Create(id), static (cart, identifier) => cart with { Id = identifier });
    }

    public static Entity<Cart> SetTotalAmountWithoutTax(this Entity<Cart> cart, int totalAmount) 
    {
        return cart.SetValueObject(Amount.Create(totalAmount), static (cart, totalAmount) => cart with { TotalAmountWithoutTax = totalAmount });
    }

/* ... */
}

The problem of constructors

An entity with 6 fields requires a constructor with 6 parameters which is a lot... Moreover, some parameters could not be known depending on the call context. This would require creating several constructors or creating optional parameters.

To avoid multiplying the constructors, we will take inspiration from the builder pattern. Here is what I want to write to create a cart item:

var cartItem = new CartItemBuilder()
    .WithId("45xxsDg1=")
    .WithProductId("ne252TJqAWk3")
    .WithAmount(25)
    .Build();

How to do that? First, let's create the CartItem Entity :

public record Item 
{
    public Identifier Id { get; init; }
    public Identifier ProductId { get; init; }
    public Amount Amount { get; init; }
      
    private Item()
    {
        Id = new Identifier();
        ProductId = new Identifier();
        Amount = new Amount();
    }

    public static Entity<Item> Create(string id, string productId, int amount)
    {
        /* Add some verifications here if needed by returning an Error 
         * which will be automatically converted in an invalid Entity.
         * Eg: return new Error("Error !!")
         */

        return Entity<Item>.Valid(new Item())
            .SetId(id)
            .SetProductId(productId)
            .SetAmount(amount);
    }
}

And let's create the builder:

public record ItemBuilder
{
    private string _id { get; set; }
    private string _productId { get; set; }
    private int _amount { get; set; }

    public ItemBuilder WithId(string id) { _id = id; return this; }
    public ItemBuilder WithProductId(string productId) { _productId = productId; return this; }
    public ItemBuilder WithAmount(int amount) { _amount = amount; return this; }

    public Entity<Item> Build() => Item.Create(_id, _productId, _amount);
}

Et voilà! :) We now have a fully functional, immutable and containerized entity. It is the best approach I found so far, if you have new ideas to improve this, please let me know!

Next, we will talk about the application layer: containerization and binding will reveal their full power for chaining functions and error management, without writing tons of conditional logic.

You can find the complete source code of this article on my Github.

Some code generation

Writing all these setters and builders can be cumbersome work. So I wrote some Roslyn generators for that, have a look if you are interested: Grenat.Functional.DDD.Generators.

Summary

  • An entity should be composed of entities, a list of entities or value objects, but never primitives.

  • In a functional approach :

    • Use C# records to create immutable entities.

    • All behaviors take the form of extension methods that return a copy of the new entity.

  • Use my library Grenat.Functional.DDD to help you with container composition and immutability.

About the cover image

This pretty little nebula located in the Cygnus constellation seems to make its way in the middle of a field of stars… The total exposure time is about 15 hours.

See it in large size in my portfolio.