Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jordiaragonzaragoza/JordiAragonZaragoza.SharedKernel/llms.txt

Use this file to discover all available pages before exploring further.

Value objects are immutable domain concepts defined entirely by their attributes rather than by a unique identity. Two Money instances both representing €9.99 are interchangeable — what matters is what they are, not which object they are. BaseValueObject provides a battle-tested base class that enforces structural equality through a single abstract method, GetEqualityComponents(), and handles ORM-proxy edge cases transparently so your domain model stays clean.

BaseValueObject

BaseValueObject implements IComparable and IComparable<BaseValueObject>. Subclasses must override only GetEqualityComponents() to declare which fields constitute the value object’s identity.
[Serializable]
public abstract class BaseValueObject : IComparable, IComparable<BaseValueObject>
{
    // Equality operators delegate to Equals()
    public static bool operator ==(BaseValueObject a, BaseValueObject b);
    public static bool operator !=(BaseValueObject a, BaseValueObject b);

    public override bool Equals(object? obj);
    public override int GetHashCode();

    public int CompareTo(object? obj);
    public int CompareTo(BaseValueObject? other);

    // Guard helper — throws BusinessRuleValidationException when rule.IsBroken()
    protected static void CheckRule(IBusinessRule rule);

    // Declare the fields that define equality
    protected abstract IEnumerable<object> GetEqualityComponents();
}

Key members

MemberBehaviour
== / !=Null-safe structural equality via Equals.
EqualsCompares GetEqualityComponents() sequences. Uses GetUnproxiedType to resolve ORM proxy types before comparing, so Castle.Proxies.MoneyProxy == Money works correctly.
GetHashCodeComputed once from GetEqualityComponents() and cached in cachedHashCode using a multiply-and-accumulate strategy (current * 23) + component.GetHashCode().
CompareToCompares component-by-component via IComparable. Type names are compared ordinally when the types differ.
CheckRuleStatic guard identical to the one on BaseEntity — throws BusinessRuleValidationException if the rule is broken.
GetEqualityComponents()Abstract. Yield every field that contributes to value identity.

ORM proxy awareness

EF Core and NHibernate may wrap value objects in dynamically generated proxy classes at runtime. GetUnproxiedType strips the proxy back to the real type so that equality checks are never confused by proxy-generated class names:
internal static Type GetUnproxiedType(object obj)
{
    const string EFCoreProxyPrefix  = "Castle.Proxies.";
    const string NHibernateProxyPostfix = "Proxy";

    var type       = obj.GetType();
    var typeString = type.ToString();

    if (typeString.Contains(EFCoreProxyPrefix, StringComparison.InvariantCulture)
        || typeString.EndsWith(NHibernateProxyPostfix, StringComparison.InvariantCulture))
    {
        return type.BaseType!;
    }

    return type;
}
You never call this method directly; it is invoked automatically inside Equals and CompareTo.

Implementing a value object — Money

public sealed class Money : BaseValueObject
{
    public Money(decimal amount, string currency)
    {
        CheckRule(new AmountMustBePositiveRule(amount));
        CheckRule(new CurrencyMustBeValidIsoCodeRule(currency));

        this.Amount   = amount;
        this.Currency = currency;
    }

    // Required by EF Core
    private Money() { }

    public decimal Amount   { get; private init; }
    public string  Currency { get; private init; } = string.Empty;

    // Convenience factory
    public static Money Of(decimal amount, string currency) => new(amount, currency);

    // Express domain semantics as methods, not operators
    public Money Add(Money other)
    {
        CheckRule(new MoneyMustHaveSameCurrencyRule(this, other));
        return new Money(this.Amount + other.Amount, this.Currency);
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return this.Amount;
        yield return this.Currency;
    }
}
Usage:
var price    = Money.Of(9.99m,  "EUR");
var shipping = Money.Of(2.50m,  "EUR");
var total    = price.Add(shipping);   // Money(12.49, "EUR")

Console.WriteLine(price == Money.Of(9.99m, "EUR")); // True — structural equality
Console.WriteLine(price == shipping);               // False

Typed IDs as value objects — BaseEntityId<TIdType>

BaseEntityId<TIdType> extends BaseValueObject and models a strongly-typed entity identifier. Using a dedicated ID class instead of a raw Guid or int prevents passing the wrong kind of ID to a method.
public abstract class BaseEntityId<TIdType> : BaseValueObject, IEntityId<TIdType>
    where TIdType : notnull
{
    protected BaseEntityId(TIdType value);

    // Required by EF Core
    protected BaseEntityId();

    public TIdType Value { get; init; }

    // Implicitly convert to the underlying primitive — no .Value call needed
    public static implicit operator TIdType(BaseEntityId<TIdType> self);

    public override string? ToString();

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return this.Value;
    }
}
The constructor guards against default values (e.g. Guid.Empty), so a stray new OrderId(Guid.Empty) throws immediately rather than silently propagating a sentinel value.

Example — OrderId

public sealed class OrderId : BaseAggregateRootId<Guid>
{
    public OrderId(Guid value) : base(value) { }

    public static OrderId New() => new(Guid.NewGuid());
}

// BaseAggregateRootId<TId> is a thin specialisation of BaseEntityId<TId>:
// public abstract class BaseAggregateRootId<TId> : BaseEntityId<TId>
//     where TId : notnull
Because of the implicit conversion operator you can pass an OrderId anywhere a Guid is expected:
OrderId orderId = OrderId.New();
Guid rawGuid = orderId;   // implicit — no .Value needed
Use strongly-typed IDs derived from BaseEntityId<TIdType> (or BaseAggregateRootId<TIdType>) for every aggregate and entity. This eliminates primitive obsession and makes method signatures self-documenting: void Ship(OrderId orderId) is far safer than void Ship(Guid orderId).

Build docs developers (and LLMs) love