Table of Contents

Class ValueObject

Namespace
Trellis
Assembly
Trellis.Core.dll

Base class for value objects in Domain-Driven Design. A value object represents a descriptive aspect of the domain with no conceptual identity. Value objects are immutable, defined by their attributes, and support structural equality.

public abstract class ValueObject : IComparable<ValueObject>, IComparable, IEquatable<ValueObject>
Inheritance
ValueObject
Implements
Derived
Inherited Members
Extension Methods

Examples

Simple value object example:

public class Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string PostalCode { get; }

    private Address(string street, string city, string state, string postalCode)
    {
        Street = street;
        City = city;
        State = state;
        PostalCode = postalCode;
    }

    // Factory method with validation
    public static Result<Address> TryCreate(
        string street, string city, string state, string postalCode) =>
        Result.Ok((street, city, state, postalCode))
            .Ensure(x => !string.IsNullOrWhiteSpace(x.street), 
                   Error.InvalidInput.ForField(field: "street", code: "invalid", detail: "Street is required"))
            .Ensure(x => !string.IsNullOrWhiteSpace(x.city),
                   Error.InvalidInput.ForField(field: "city", code: "invalid", detail: "City is required"))
            .Map(x => new Address(x.street, x.city, x.state, x.postalCode));

    // Define what makes two addresses equal
    protected override void GetEqualityComponents(ref EqualityComponents components)
    {
        components.Add(Street);
        components.Add(City);
        components.Add(State);
        components.Add(PostalCode);
    }

    // Domain behavior
    public string GetFullAddress() => 
        $"{Street}, {City}, {State} {PostalCode}";
}

// Usage
var address1 = Address.TryCreate("123 Main St", "Springfield", "IL", "62701")
    .Match(a => a, e => throw new System.InvalidOperationException(e.GetDisplayMessage()));
var address2 = Address.TryCreate("123 Main St", "Springfield", "IL", "62701")
    .Match(a => a, e => throw new System.InvalidOperationException(e.GetDisplayMessage()));

// Structural equality
address1 == address2; // true - same attributes

Value object with rich behavior:

public class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }

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

    public static Result<Money> TryCreate(decimal amount, string currency = "USD") =>
        Result.Ok((amount, currency))
            .Ensure(x => x.amount >= 0, Error.InvalidInput.ForField(field: "amount", code: "invalid", detail: "Amount cannot be negative"))
            .Ensure(x => x.currency.Length == 3, 
                   Error.InvalidInput.ForField(field: "currency", code: "invalid", detail: "Currency must be 3-letter ISO code"))
            .Map(x => new Money(x.amount, x.currency.ToUpperInvariant()));

    protected override void GetEqualityComponents(ref EqualityComponents components)
    {
        components.Add(Amount);
        components.Add(Currency);
    }

    // Domain operations return new instances (immutability)
    public Result<Money> Add(Money other) =>
        Currency != other.Currency
            ? Result.Fail<Money>(Error.InvalidInput.ForRule(code: ValidationCodes.MoneyCurrencyMismatch, detail: $"Cannot add {other.Currency} to {Currency}"))
            : Result.Ok(new Money(Amount + other.Amount, Currency));

    public Money Multiply(decimal factor) =>
        new Money(Amount * factor, Currency);
}

Derived value object example:

public class InternationalAddress : Address
{
    public string Country { get; }

    private InternationalAddress(
        string street, string city, string state, 
        string postalCode, string country) 
        : base(street, city, state, postalCode)
    {
        Country = country;
    }

    // Include base components plus additional ones
    protected override void GetEqualityComponents(ref EqualityComponents components)
    {
        base.GetEqualityComponents(ref components);
        components.Add(Country);
    }
}

Remarks

Value objects are one of the three main building blocks in DDD (along with Entities and Aggregates). Key characteristics:

  • Identity: Defined by attribute values, not by a unique identifier
  • Immutability: Once created, a value object's state cannot change
  • Equality: Two value objects with the same attributes are considered equal
  • Interchangeability: Value objects with equal attributes can be freely substituted
  • Side-effect free: Methods on value objects don't modify state, they return new instances

Value Objects vs. Entities:

  • Value Object: Defined by attributes (e.g., Address, Money, EmailAddress)
  • Entity: Defined by identity (e.g., Customer, Order, Product)

Benefits of using value objects:

  • Type safety: EmailAddress is more expressive than string
  • Validation: Encapsulate validation logic in the value object
  • Rich behavior: Add domain-specific methods (e.g., Money.Add, Temperature.ToFahrenheit)
  • Immutability: Prevents accidental state changes
  • Testability: Pure functions are easy to test

When to use value objects:

  • The concept measures, quantifies, or describes something in the domain
  • It can be modeled as immutable
  • It models a conceptual whole by grouping related attributes
  • Equality should be based on the whole set of attributes
  • There's domain behavior associated with the concept

Methods

CompareTo(ValueObject?)

Compares the current value object with another value object of the same type.

public virtual int CompareTo(ValueObject? other)

Parameters

other ValueObject

The value object to compare with this instance.

Returns

int

A value less than zero if this instance is less than other; zero if they are equal; or greater than zero if this instance is greater than other.

Remarks

Components are compared in order. The first non-equal component determines the result. Null sorts before non-null, so a non-null value object is greater than null. Components with different runtime types at the same position are ordered by type name before same-typed components use their native comparison. This enables value objects to be sorted and used in ordered collections.

Exceptions

ArgumentException

Thrown when other is not of the same type as this instance.

Equals(object?)

Determines whether the specified object is equal to the current value object.

public override bool Equals(object? obj)

Parameters

obj object

The object to compare with the current value object.

Returns

bool

true if the specified object is a value object of the same type with equal components; otherwise, false.

Equals(ValueObject?)

Determines whether the specified value object is equal to the current value object. Two value objects are equal if they have the same type and all equality components are equal.

public bool Equals(ValueObject? other)

Parameters

other ValueObject

The value object to compare with the current value object.

Returns

bool

true if the value objects have the same type and equal components; otherwise, false.

Remarks

This implements structural equality based on the components returned by GetEqualityComponents(ref EqualityComponents). Value objects of different types are never equal, even if they have the same component values.

GetEqualityComponents(ref EqualityComponents)

When overridden in a derived class, adds the components that define equality for this value object.

protected abstract void GetEqualityComponents(ref EqualityComponents components)

Parameters

components EqualityComponents

The sink that collects this value object's equality components.

Examples

protected override void GetEqualityComponents(ref EqualityComponents components)
{
    components.Add(Street);
    components.Add(City);
    components.Add(PostalCode);
}

Remarks

This method is used by Equals(ValueObject?), GetHashCode(), and CompareTo(ValueObject?) to determine equality and ordering. Components must be added in a consistent order.

Guidelines:

  • Add all properties that define the value object's identity
  • For derived classes, call base.GetEqualityComponents(ref components) first
  • Add components in a consistent, deterministic order
  • Do not allocate: the sink exists so comparisons stay allocation-free

GetHashCode()

Returns a hash code for this value object based on its equality components.

public override int GetHashCode()

Returns

int

A hash code combining all equality components.

Remarks

The hash code is cached for performance since value objects are immutable. This ensures consistent hash codes for the lifetime of the object and improves performance when used as dictionary keys or in hash-based collections.

The cache reserves 0 as its "not yet computed" sentinel, so components that combine to exactly 0 report 1 instead. Equal instances still report equal hash codes. Hash codes are not stable across processes and must not be persisted.

MaybeComponent<T>(Maybe<T>)

Converts a Maybe<T> to an IComparable for use in GetEqualityComponents(ref EqualityComponents). Returns the value if present, or null if empty.

protected static IComparable? MaybeComponent<T>(Maybe<T> maybe) where T : notnull, IComparable

Parameters

maybe Maybe<T>

The optional value to convert.

Returns

IComparable

The underlying value as IComparable, or null if the Maybe is empty.

Type Parameters

T

The type of the optional value. Must implement IComparable.

Examples

protected override void GetEqualityComponents(ref EqualityComponents components)
{
    components.Add(Street);
    components.Add(MaybeComponent(Apartment));
}

Remarks

Prefer Add<T>(Maybe<T>), which does the same conversion inline.

Operators

operator ==(ValueObject?, ValueObject?)

Determines whether two value objects are equal.

public static bool operator ==(ValueObject? a, ValueObject? b)

Parameters

a ValueObject

The first value object to compare.

b ValueObject

The second value object to compare.

Returns

bool

true if both are null or have equal components; otherwise, false.

operator >(ValueObject?, ValueObject?)

Determines whether the first value object is greater than the second.

public static bool operator >(ValueObject? left, ValueObject? right)

Parameters

left ValueObject

The first value object to compare.

right ValueObject

The second value object to compare.

Returns

bool

true if left is greater than right; otherwise, false.

Remarks

This operator uses the same ordering as CompareTo(ValueObject?): null sorts before non-null, and different runtime types are not comparable.

Exceptions

ArgumentException

Thrown when both operands are non-null and have different runtime types.

operator >=(ValueObject?, ValueObject?)

Determines whether the first value object is greater than or equal to the second.

public static bool operator >=(ValueObject? left, ValueObject? right)

Parameters

left ValueObject

The first value object to compare.

right ValueObject

The second value object to compare.

Returns

bool

true if left is greater than or equal to right; otherwise, false.

Remarks

This operator uses the same ordering as CompareTo(ValueObject?): null sorts before non-null, and different runtime types are not comparable.

Exceptions

ArgumentException

Thrown when both operands are non-null and have different runtime types.

operator !=(ValueObject?, ValueObject?)

Determines whether two value objects are not equal.

public static bool operator !=(ValueObject? a, ValueObject? b)

Parameters

a ValueObject

The first value object to compare.

b ValueObject

The second value object to compare.

Returns

bool

true if the value objects have different components; otherwise, false.

operator <(ValueObject?, ValueObject?)

Determines whether the first value object is less than the second.

public static bool operator <(ValueObject? left, ValueObject? right)

Parameters

left ValueObject

The first value object to compare.

right ValueObject

The second value object to compare.

Returns

bool

true if left is less than right; otherwise, false.

Remarks

This operator uses the same ordering as CompareTo(ValueObject?): null sorts before non-null, and different runtime types are not comparable.

Exceptions

ArgumentException

Thrown when both operands are non-null and have different runtime types.

operator <=(ValueObject?, ValueObject?)

Determines whether the first value object is less than or equal to the second.

public static bool operator <=(ValueObject? left, ValueObject? right)

Parameters

left ValueObject

The first value object to compare.

right ValueObject

The second value object to compare.

Returns

bool

true if left is less than or equal to right; otherwise, false.

Remarks

This operator uses the same ordering as CompareTo(ValueObject?): null sorts before non-null, and different runtime types are not comparable.

Exceptions

ArgumentException

Thrown when both operands are non-null and have different runtime types.