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 :
Unwrap the inner value of the
Entity<T>
to modify. Then :Unwrap the inner value of the container (an
Entity<V>
, aValueObject<V>
, anOption<V>
, ...) that is passed as a parameter,They modify the targeted property of
Entity<T>
thanks the setter function,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, Error
s 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.