Domain Primitives I: easily declaring domain primitives

TL;DR: A domain primitive base class makes declaring domain primitives a breeze.

After My Take on Domain Primitives, this is the first post showing how working with domain primitives can be made as easy as possible in C#.

You can find the code in my DomainPrimitives github repo.

I suggest the following requirements for a C# type representing a domain primitive:

  • validity must be enforced at creation time
  • value must be immutable
  • value-type semantics with consideration of the domain primitive type, i. e. Equals, GetHashCode, ideally also == and !=
  • an instance of the domain primitive can be used everywhere the underlying value type can be used
  • explicit nullability
  • easily debuggable

At first it looks like a readonly struct would be a natural choice, since it is there to represent immutable value semantics. However, Equals and GetHashCode and along with them == and != would still need to be overridden to respect the type of the domain primitive. We would not want an instance of the type MyPrimitive1 to be considered equal to an instance of the type MyPrimitive2 just because they happen to have the same underlying value type. In addition, the end result should enable developers to declare domain primitives with as little overhead as possible. Since structs do not allow inheritance, all the method overrides would have to be declared over and over in every domain primitive. We could use a template for that, but still, it seems like too much boilerplate code and we have to deal with updates to that template.

The performance penalty of not using a struct and hence always boxing the underlying value should be negligible for almost all applications. C# 8 gave us explicit reference type nullability. Furthermore, using a class avoids any ugliness with parameterless struct constructors.

Although C# 9 record types seem like a good fit for domain primitives, they cannot be used to define a domain primitive base type due to the ToString gotcha.

Class it is then. An abstract class fulfilling all the requirements above could look like this:

[DebuggerDisplay("{value} <{this.GetType().Name,nq}>")]
public abstract class KindOf<T>
{
    public T Value { get; }

    protected KindOf(T value)
    {
        this.Value = value ?? throw new ArgumentNullException(nameof(value));
    }

    public override bool Equals(object obj) =>
        obj != null &&
        obj.GetType() == GetType() &&
        Equals(Value, ((KindOf<T>)obj).Value);

    public override int GetHashCode() =>
        HashCode.Combine(GetType(), Value);

    public override string ToString() => Value.ToString();

    public static implicit operator T(KindOf<T> kindOfT) => kindOfT.Value;

    public static bool operator ==(KindOf<T> left, KindOf<T> right) =>
        left is null
            ? right is null
            : left.Equals(right);

    public static bool operator !=(KindOf<T> left, KindOf<T> right) => !(left == right);
}

Note that Value is public despite already having an implicit conversion operator defined. This makes things easier in situations where implicit conversion is not applicable, like calling methods on the underlying type, e. g. firstName.Value.Substring(...) instead of having to do ((string)firstName).Substring(...).

This fulfills all the requirements above with the caveat that == and != will not respect the domain primitive type when both instances are null. The operators need to be defined though, because otherwise the compiler would, upon encountering == or !=, use the implicit conversion to the underlying value type and use the operator on the underlying value type, not considering the domain primitive type. We could resolve this issue by defining the operators on the actual domain primitive types, but that would be detrimental to our goal of making working with them as easy as possible.

We could try getting the domain primitive type information by adding the good ol’ home-made wannabe-self-refencial recursive type parameter, à la KindOf<T, TPrimitive> where TPrimitive : KindOf<T, TPrimitive>. This would also give us the ability to implement IEquatable<TPrimitive> in the base class if we really wanted to. The result then would look like this:

[DebuggerDisplay("{value} <{this.GetType().Name,nq}>")]
public abstract class KindOf<T, TPrimitive> : IEquatable<TPrimitive>
    where TPrimitive : KindOf<T, TPrimitive>
{
    public T Value { get; }

    protected KindOf(T value)
    {
        if (this.GetType() != typeof(TPrimitive))
        {
            throw new ArgumentException(                
                $"type parameter {nameof(TPrimitive)} must be {this.GetType().Name}, but was {typeof(TPrimitive).Name}");
        }

        this.Value = value ?? throw new ArgumentNullException(nameof(value));
    }

    public override bool Equals(object obj) =>
        obj is TPrimitive other &&
        Equals(other);

    public override int GetHashCode() =>
        HashCode.Combine(typeof(TPrimitive), Value);

    public override string ToString() => Value.ToString();

    public bool Equals(TPrimitive other) =>
        other != null &&
        Equals(Value, other.Value);

    public static implicit operator T(KindOf<T, TPrimitive> kindOfT) => kindOfT.Value;

    public static bool operator ==(KindOf<T, TPrimitive> left, KindOf<T, TPrimitive> right) =>
        left is null
            ? right is null
            : left.Equals(right);

    public static bool operator !=(KindOf<T, TPrimitive> left, KindOf<T, TPrimitive> right) =>
        !(left == right);

    public static bool operator ==(KindOf<T, TPrimitive> left, object right) =>
        right is TPrimitive other && left == other;

    public static bool operator !=(KindOf<T, TPrimitive> left, object right) =>
        !(left == right);
}

Note the additional == and != operators. Without them, the compiler, upon encountering instances of different domain primitive types with the same underlying value type, would otherwise use the implicit operator to cast to the underlying type before applying == or !=, making them no longer type-aware:

Is the extra complexity worth it? The additional type parameter will make things less readable and is itself not fool-proof (we could write class FirstName : KindOf<string, LastName> without the compiler complaining). So let us stick with the original KindOf<T> proposal for the moment.

We can now easily define actual domain primitive types by only filling in the initial validation:

public sealed class RegistryNumber : KindOf<string>
{
    public RegistryNumber(string value)
        : base(value)
    {
        if (!Regex.IsMatch(value, "^[0-9|A-Z]{9}$"))
        {
            throw new ArgumentException("must be 9-digit alphanumeric string", nameof(value));
        }
    }
}

For now we resorted to throwing exceptions on invalid values. However in the introductory post on domain primitives I mentioned something along the lines of forcing developers to validate. Exceptions can easily be ignored by developers and then crash the application at run-time. In the next post we will try to actually force developers to deal with the invalid cases.

Again, you can find the code in my DomainPrimitives github repo.

1 Comment

Comments are closed