Value objectsValueObject, SingleValueObject, and value equality
No narration yet
Module 3 · Lesson 212 min

ValueObject, SingleValueObject, and value equality

There are two base classes, and picking the right one is a small but real convention. SingleValueObject<TSelf, TValue> is for a value object wrapping ONE primitive: a flight number (string), a bounded percentage (decimal). It gives you Value, equality, and ToString for free, so a subclass only needs a private constructor and a Create.

SingleValueObject gives Value + equality free (SingleValueObject.cs)
public abstract class SingleValueObject<TSelf, TValue> : ValueObject<TSelf>
    where TSelf : SingleValueObject<TSelf, TValue>
{
    public TValue Value { get; }
    protected SingleValueObject(TValue value) => Value = value;

    protected sealed override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Value;
    }

    public override string ToString() => Value?.ToString() ?? string.Empty;
}

For a value object with MORE than one field, extend ValueObject<T> directly and override GetEqualityComponents, yielding each field. Equality is then a SequenceEqual over those components: two value objects are equal when all their components match. FlightNoteVo (flight key + body) is the two-field case.

A multi-field VO's equality (FlightNoteVo.cs)
protected override IEnumerable<object?> GetEqualityComponents()
{
    yield return EjdpUniqueFlightKey;
    yield return Body;
}

One more real detail worth copying. Some single-value VOs expose a second factory: public static T From(value) => Create(value).Value;. It's a shorthand for domain-internal code and unit tests that throws if the value is invalid. The real XML doc is blunt: "Do NOT use at application boundaries, prefer Create and inspect the Result." Two factories, two trust levels: Create at the edge, From only where you already know the value is good.

Practice

Try it yourself

Do

Write a SingleValueObject

Feel how little a single-value VO needs. Model it on FlightNumberVo above.

Tick every step to confirm you did it.

Quiz

Pick the base class

You're modelling a Money value object with an amount (decimal) AND a currency (string). Which base?

Recall

Create versus From

Know which factory goes where.

A VO has both a Create and a From factory. Which do you call when parsing a value from an incoming API request, and why?