DDD Value Objects as C# Records: The Missing Manual
Tags: csharp, ddd, domaindrivendesign
Published: 2022-01-03 15:14:45

[C# Records](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are very handy if you need to implement [Value Objects](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf#page=19), because they offer support for value-equality out of the box. [Positional Records](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#positional-syntax-for-property-definition) and the `with`-operator ease the implementation and usage of immutable data-types. But beware, records are not a template for Value Objects but rather a toolbox. Therefore you should keep caution which features you use. Otherwhise, the compiler may create code that allows you to manipulate your 'immutable' or to create objects that have an invalid state.

As a brief recap, here are the most important properties of a [Value Object](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf#page=19) in the context of Domain-Driven Design (DDD):
* Value Objects contain business logic (e.g. restrictions/constraints on their properties)
* Value Objects are immutable
* Value Objects support value-equality (i.e. two instances are equal when all of their properties are equal)

Luckily, if we use Records we do not need to think much about equality. However, we do not get the immutability and enforcement of our restrictions from the framework for free. In this post, I'll show you some pitfalls and what you can do about them so that at the end of the day, you have a solid implementation of a Value object.

## Validation of Constraints
To prevent an object from being created with an invalid state, the properties must be validated before an instance is created. The checks must therefore be implemented either in the initializer or in the constructor.

Lets assume we want to model the measured value of a thermometer. Obviously, a temperature cannot be below absolute zero. As this condition can be validated against the measured reading, the value can be validated within the initializer.
```csharp
public record TemperatureCelsius
{
    private readonly Decimal _value;
    public Decimal Value
    {
        get => _value;
        init => _value = value < -273.15m ? throw new ArgumentOutOfRangeException() : value;
    }
}
```
As an example of a dependency at validation-time, assume that the temperature unit is variable. Consequently, the correct minimum value must be taken into account when validating the temperature.
```csharp
public record Temperature
{
    public Temperature(Decimal degrees, TemperatureUnit unit)
    {
        switch (unit)
        {
            case TemperatureUnit.Celsius when degrees < -273.15m: throw new ArgumentOutOfRangeException();
            case TemperatureUnit.Fahrenheit when degrees < -459.67m: throw new ArgumentOutOfRangeException();
            case TemperatureUnit.Kelvin when degrees < 0m: throw new ArgumentOutOfRangeException();
        }

        Degrees = degrees;
        Unit = unit;
    }

    public Decimal Degrees { get; private init; }
    public TemperatureUnit Unit { get; private init; }
}
```

Could we have implemented the check in the initializers as well? Unfortunately no, because initializers can be called in any order. If the property `Degrees` is then initialized before `Unit`, we cannot perform a meaningful check for undercutting the zero point.

What does this mean for the implementation of a value-object?
* If __all properties can be validated in isolation__, then you can implement the validation in initializers.
* If there are __dependencies at validation-time__ between properties, then you must do the validation in the constructor and properties must not have public initializers.
* If you __don't need to do any validation__, then the value-object may have public initializers. In this case you should consider using [Positional Records](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#positional-syntax-for-property-definition) to keep the code short and readable.

_(In my opinion, modeling a measurement value like in the above example ('`Temperature`') is suboptimal for a reallife application. If possible, only measurement unit should be used within a context. The conversion to other units then happens at the context boundaries.)_

### A little about the behavior of the with-operator
The `with`-operator can be used to create a new record with modified properties. The initializers are called for the properties that are set within the block and the values are copied for all other properties. This means that the validations we implemented in the initializer will be applied automatically. So we don't have to care about that use case during the implementation.

If a property does not have a public initializer, then the property cannot be changed when the `with`-operator is called.
```csharp
// Using the type 'Temperature' from the example above.
var celsius = new Temperature(34m, TemperatureUnit.Celsius);
var kelvin = celsius with { Unit = TemperatureUnit.Kelvin }; // Error CS0272: Property has no accessible setter.
```

## Immutability
Creating an immutable record is not difficult, as long as your domain model is designed in such a way that a value-object contains only other Value Objects or primitive data types (`int`, `string`, ...). Then you just have to make sure that the properties and backing-fields are read-only. You achieve this by marking your [fields](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/members) with `readonly` and assure that properties have none or private setters.

However, if your model specifies that a Value object holds multiple objects in a single property, you must be careful. Arrays and [Lists](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1) are mutable. If an instance of these types is declared read-only, that reference cannot be changed, but the contents of the object can. Therefore, you should make sure that the container does not allow manipulation of its contents.
```csharp
private readonly int[] array;

public void Example()
{
    array[1] = 2; // Assignment is possible
    array = new int[3]; // Error CS0191: Cannot assign to read-only field
}
```

In theory, you can use [Immutable Collections](https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable) provided by the framework. However, these have the disadvantage that they [violate](https://en.wikipedia.org/wiki/Liskov_substitution_principle) [Liskov's substitution principle](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.readonlycollection-1.system-collections-generic-icollection-t--add) and that their equality is not determined based on their elements (see below).

What does this mean for the implementation of a value-object?
* Properties must not have setters (but may have initializers).
* Fields should be declared as `readonly`. This ensures that no method can change the state of the object.
* Use immutable containers only.

## Value Equality
The C# compiler automatically creates an `Equals` and `GetHashCode` method for a record that takes all properties of two instances into account. However, this only works if all properties also support value equality. This is the case for primitive data types and (correctly implemented) Value objects. As a container, we may only use data structures that support value equality.

Unfortunately, the Immutable Collections from the framework do not help us at this point, as the following example with the (vicariously selected) `ImmutableArray` class shows.
```csharp
public record ColorGradient(System.Collections.Immutable.ImmutableArray<Color> Colors);
```
```csharp
[Fact]
public void Equality()
{
    var gradient1 = new ColorGradient(new[] { Color.Black, Color.Green, Color.White }.ToImmutableArray());
    var gradient2 = new ColorGradient(new[] { Color.Black, Color.Green, Color.White }.ToImmutableArray());

    Assert.NotEqual(gradient1, gradient2); // System.Collections.Immutable.ImmutableArray does not support value-equality.
}
```
When using a container that satisfies value equality (e.g. [ValueArrays](https://www.nuget.org/packages/ValueArrays/)) the type `ColorGradient2` behaves as expected.
```csharp
public record ColorGradient2(ValueArrays.ValueArray<Color> Colors);
```
```csharp
[Fact]
public void Equality()
{
    var gradient1 = new ColorGradient2(new ValueArrays.ValueArray<Color>(new[] { Color.Black, Color.Green, Color.White }));
    var gradient2 = new ColorGradient2(new ValueArrays.ValueArray<Color>(new[] { Color.Black, Color.Green, Color.White }));

    Assert.Equal(gradient1, gradient2);
}
```

What does this mean for the implementation of a value-object?
* To take advantage of the value equality of records, all properties must belong to one of the following categories:
  * Value Objects
  * Primitive data types
  * Container that support value equality (e.g. [ValueArrays](https://www.nuget.org/packages/ValueArrays/))

## Conclusion
Regardless of whether you implement a value-object as a class or record, you must ensure that there can be no invalid or mutable objects. The necessary building blocks (initializer, constructor, 'no setters') were introduced in the sections "Validation of Constraints" and "Immutability". Why you should be careful when using containers has been explained in the sections "Immutability" and "Value Equality".

If you follow these tips, the built-in value equality of records will work. Also, you don't need to implement `Equals` or `GetHashCode` (and tests for them). Therefor you can fully focus on the implementation of your business logic...


## Further Reading
* [Domain-Driven Design Reference -- Eric Evans](https://www.domainlanguage.com/ddd/reference/)
* [C#9 on the Record -- Mads Torgensen](https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/)
* [Records (C# Reference) -- MSDN](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#positional-syntax-for-property-definition)
* [Create record types -- MSDN Tutorial](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records)
* [Namespace System.Collections.Immutable - MSDN](https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable)
* [ValueArrays -- Nuget](https://www.nuget.org/packages/ValueArrays/)