Nulls in Value Objects

Today, we’ll discuss an interesting use case of handling nulls in value objects. Should you put null inside the value objects themselves or decorate those value objects using the nullable reference type notation (? or Maybe)?

1. The null choice

The topic of this article comes from a couple of questions I received during the last month or two. Here’s one of them that summarizes the subject pretty well:

If you have a value object in which all properties can be null, should that value object itself be null or should the value object be created with the properties inside of it set to null?

To illustrate this question, let’s say that we have the following Customer class:

public class Customer : Entity
{
    public PhoneNumber Phone { get; }
}

public class PhoneNumber : ValueObject
{
    public string Number { get; }
}

Let’s also say that customers are not required to fill out their phone number when registering, and so the corresponding field should be nullable. How can we reflect this requirement in the domain model?

There are two ways to do so:

  • Make the PhoneNumber.Number field nullable — This will put the null-bearing responsibility onto the value object itself.

  • Make the Customer.Phone field nullable — This will put that responsibility onto the overarching entity.

In other words, the choice comes down to this dilemma:

Nulls in Value Objects

So, which approach is better?

2. It’s all about invariants

The choice depends on the specifics of your domain model. If Nullable<T> makes sense in the context of the ValueObject, then choose the first approach (ValueObject<Nullable<T>>); otherwise, go with Nullable<ValueObject<T>>.

In other words, you need to ask yourself whether the null (or empty) value is valid for the given value object. For example, a phone number can’t be an empty string. Such a string would violate PhoneNumber's invariants, which are (most likely) the requirement to contain exactly 10 digits.

And so, the nullability should be pushed from PhoneNumber to Customer

public class Customer : Entity
{
    public Maybe<PhoneNumber> Phone { get; }
}

while PhoneNumber should always protect itself from containing an empty value:

public class PhoneNumber : ValueObject
{
    public string Number { get; }

    private PhoneNumber(string number)
    {
        Number = number;
    }

    public static Result<PhoneNumber> Create(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return Result.Failure<PhoneNumber>("Phone number is invalid");

        // Other checks

        return Result.Success(new PhoneNumber(input));
    }
}

Notice that I’m using a Maybe class to show that Customer.Phone field is nullable. The alternative in C# would be to use the new nullable reference types notation:

public PhoneNumber? Phone { get; }

(I have an article in my drafts on pros and cons of both approaches.)

3. Examples of choosing ValueObject<Nullable<T>>

Notice that when possible, it is preferable to choose ValueObject<Nullable<T>> over Nullable<ValueObject<T>> because it reduces the number of nulls in your domain model. Of course, you can only do so when the null value is meaningful for this particular value object.

Null object design pattern

The preference of ValueObject<Nullable<T>> over Nullable<ValueObject<T>> is what the Null object pattern advocates for as well.

Good examples of when this is possible are OrderQuantity or Dollars value objects — both can be zero by default.

A more complex example is a multi-property value object, such as Money that consists of Currency and Amount fields:

public class Money : ValueObject
{
    public static Money Zero = new Money(Currency.None, 0);
    
    public Currency Currency { get; }
    public decimal Amount { get; }

    private Money(Currency currency, decimal amount)
    {
        Currency = currency;
        Amount = amount;
    }
}

To represent the zero value, we can choose a special kind of currency (None in the above example), and use it in combination with the 0 amount.

4. Nulls that depend on the context

Sometimes, a value object should be nullable in one entity but non-nullable in another entity. How to best handle such a use case?

If the value object is simple (i.e. consists of only 1 property), then the difference is a no-brainer. For example, let’s say we have 2 entities, Customer and Employee. Let’s also say that all employees must have a phone number, while customers don’t have this requirement:

public class Customer : Entity
{
    public Maybe<PhoneNumber> Phone { get; }
}

public class Employee : Entity
{
    public PhoneNumber Phone { get; }
}

The difference between the two entities is simple and straightforward; it is the presence of the Maybe type.

You can also introduce an additional factory method in PhoneNumber that would create a nullable value:

public class PhoneNumber : ValueObject
{
    public string Number { get; }

    private PhoneNumber(string number)
    {
        Number = number;
    }

    /* The additional factory method for convenience */
    public static Result<Maybe<PhoneNumber>> CreateNullable(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return Maybe<PhoneNumber>.None;

        return Create(input).Map(x => (Maybe<PhoneNumber>)x);
    }

    public static Result<PhoneNumber> Create(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return Result.Failure<PhoneNumber>("Phone number is invalid");

        // Other checks

        return Result.Success(new PhoneNumber(input));
    }
}

But what about more complex scenarios? Let’s say that we have the following value object:

public class Name : ValueObject
{
    public string First { get; }
    public string Last { get; }
}

What if employees must have both the first and last names filled out, but for customers, these fields are optional? How can this be implemented?

We can’t just make the Name property nullable in Customer, and non-nullable in Employee, like this:

public class Customer : Entity
{
    public Maybe<Name> Name { get; }
}

public class Employee : Entity
{
    public Name Name { get; }
}

This implementation doesn’t cover the scenario where the customer fills out the first name, but not the last (or vice versa); it requires the customer to either fill in both names or none of them.

So how to fix this?

You need to make a separate value object for the Customer:

public class Customer : Entity
{
    public CustomerName Name { get; }
}

public class Employee : Entity
{
    public EmployeeName Name { get; }
}

public class CustomerName : ValueObject
{
    public static readonly CustomerName Empty = new CustomerName(null, null);

    public Maybe<string> First { get; }
    public Maybe<string> Last { get; }
}

public class EmployeeName : ValueObject
{
    public string First { get; }
    public string Last { get; }
}

Such a distinction might seem wasteful. After all, is creating a second class just for the sake of making its fields nullable really worth it?

Yes, it is!

This way, you are explicitly telling the future you (and other programmers who will be reading and maintaining this code) that there’s an important difference between employee and customer names. Although structurally, both value objects are the same, the nullability requirement (and the lack thereof) is what differentiates the two; you can’t just use an EmployeeName in place of a CustomerName.

Notice that you can also introduce separate value objects for the first and last name fields, and reuse those value objects between CustomerName and EmployeeName. Whether or not you should do that depends on the complexity of your project and, in most cases, is an overkill.

5. Summary

  • The null choice comes down to the ValueObject<Nullable<T>> vs Nullable<ValueObject<T>> dilemma.

  • Choose ValueObject<Nullable<T>> over Nullable<ValueObject<T>> when possible (when the null/empty value makes sense in the context of the value object)

    • This choice will help reduce the number of nulls you work with in your system

    • The null object design pattern essentially advocates for this choice as well

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus