Domain-Driven Design - Value Objects in C#

Before you look at the Value Objects example in C#, you can see at my post on what Value Objects are in Domain-Driven Design.

Value Object base class in C#

public abstract record ValueObject<T> : IEquatable<T> where T : IEquatable<T>
  {
    protected T Value { get; }

    protected ValueObject(T value)
    {
      this.Validate(value);

      this.Value = value;
    }

    protected abstract void Validate(T value);

    public bool Equals(T? other)
    {
      return this.Value.Equals(other);
    }

    public static bool operator ==(ValueObject<T> a, T b) => a.Equals(b);

    public static bool operator !=(ValueObject<T> a, T b) => !a.Equals(b);
  }

About this code snippet:

  • It is a record: Records (C# 9) are primarily built for better supporting immutable data models.
  • It is abstract: This prevents this record from being instantiated and allows to declare abstract methods.
  • T extends from IEquatable: T is the basic value type of the Value Object, so if T extends from IEquatable, it forces that basic value type of the Value Object to be equatable.
  • It extends from IEquatable<T>: Allows comparing this Value Object with its basic data type.
  • It calls an abstract method called Validate in the constructor: So we are validating the value of the Value Object before creating it.

Value Object example in C#

public sealed record FirstName : ValueObject<string>
{
  public const UInt16 MaxLength = 50;

  public string Name => this.Value;

  private FirstName(string value) : base(value)
  {
  }

  public static FirstName Create(string value)
  {
    return new FirstName(value);
  }

  protected override void Validate(string value)
  {
    if (string.IsNullOrWhiteSpace(value))
      throw new ArgumentNullException(nameof(value));

    if (value.Length > FirstName.MaxLength)
      throw new ArgumentOutOfRangeException(nameof(value));
  }
}

About this code snippet:

  • It is sealed: Prevent another class from extends from this one.
  • It overrides the Validate method with validation rules.
  • No public constructor: Other classes cannot create instances of this class, except for nested classes.
  • To create instances of this Value Object we do not call the constructor directly, but it uses the Static Factory Method pattern instead.