Domain Primitives II: forcing developers to deal with error cases

I ended the previous post in this series with a domain primitive base class that throws exceptions from its constructor on receiving invalid values. I noted that exceptions are not transparent in C# and that the original goal from the introduction to domain primitives was to force developers to deal with invalid data.

Exceptions can be ignored by developers and will crash the application at runtime. We need a different mechanism for signalling errors. One obvious solution is to use a return value. Since constructors do not have return values we would need some method to call whenever we want to create a domain primitive. Furthermore the constructor needs to be inaccessible to prevent circumventing the new mechanism.

The return type should have a couple of properties to be useful for our purposes:

  • make it hard/impossible to get a null reference on the domain primitive instance in the error case
  • make it hard/impossible to get a null reference on a validation error in the success case
  • be easy to use with little code from within the creation method

To be compile-time-safe from these null references the references should not even exist on the returned type. The caller would then be forced to check for success or failure by pattern matching or other means.

In functional programming there are usually built-in types for this based on discriminated unions. In F# for example, this would be covered by the Result type. In C#, a pattern useful for implementing discriminated unions is to declare an abstract base type with private constructor and then the required nested subtypes. There are of course libraries for functional programming in C#, like OneOf for union types and the comprehensive SuccincT or language-ext for even more functional concepts. For the sake of demonstrating the core concepts, we will stick to the simplest self-made working solution. The existing libraries tend to have a lot of extra methods on their base types based on additional design decisions distracting from our particular use case.

Records make declaring discriminated unions in C# a lot more concise.

Erratum: No, they do not. In fact, it is not possible to create closed type hierarchies with records.

One way of declaring a CreationResult return type would be this:

public abstract record CreationResult<T>
{
    // private Ctor to prevent outside code from adding another CreationResult type
    private CreationResult()
    {
    }

    // private to prevent outside code from pattern matching
    private sealed record Success(T Instance) : CreationResult<T>;
    private sealed record Failure(Error Error) : CreationResult<T>;

    // provide Switch method for actions depending on error or success
    public void Switch(Action<T> onSuccess, Action<Error> onFailure)
    {
        if (this is Success { Instance: var instance })
        {
            onSuccess(instance);
            return;
        }
        else
        {
            onFailure(((Failure)this).Error);
        }
    }

    // provide Switch method for actions depending on error or success
    public TResult Switch<TResult>(Func<T, TResult> onSuccess, Func<Error, TResult> onFailure) =>
        this switch
        {
            Success { Instance: var instance } => onSuccess(instance),
            Failure { Error: var error } => onFailure(error),
                
            // impossible because of discriminated union pattern
            _ => throw new NotImplementedException()
        };

    // convenience conversions
    public static implicit operator CreationResult<T>(T instance) => new Success(instance);
    public static implicit operator CreationResult<T>(Error error) => new Failure(error);
    public static implicit operator CreationResult<T>(string errorDescription) => new Failure(new Error(errorDescription));
}

// placeholder type for adding more properties later (multiple errors, error codes etc.)
public sealed record Error(string Description);

Note that CreationResult<T> itself has no properties. The caller is forced to inspect the result. There are a couple of design decisions I made here that influence how an instance of CreationResult can be used. I decided to make the two subtypes private. This prevents casting and pattern matching against the result type and forces developers to use the public Switch methods to do something with the result. The Switch methods then force developers to provide a function or action for both, the success case and the error case. Curiously enough, the only way to create a CreationResult is through the implicit conversion operators.

The Switch methods would be beneficial even if we had decided to make the subtypes public and thus allow developers to type-check or pattern-match. The compiler tries to make sure that pattern matching is exhaustive. Since it does not (yet) recognize the discriminated union pattern it suggests that a default case be present. It would be annoying to add a default case everytime we deal with a CreationResult. Since there is nothing else but the Instance and the Error for which to pattern-match, providing methods to do so makes sense.

I tried to make CreationResult as restrictive as possible for now. In case we miss additional convenience features that come with pattern matching like tuple deconstruction in the further exploration of domain primitives and their usage, we can always increase accessibility. It will not be a breaking change.

Using the new type, we can refactor RegistryNumber to look like this:

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

        return new RegistryNumber(value);
    }

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

It does the job and is IMHO simple enough to be established as a pattern

But let’s try and think about if we can make it less verbose anyway. After all, the only code specific to RegistryNumber is the if-clause. Unfortunately I do not see a way to significantly reduce this overhead. At first it looks like it would be possible to at least add the static create method to the base class if we go with the KindOf<T, TPrimitive> variant from a previous post. That way we would only have to override a Validate method in our primitive class and yield our errors, like this:

    public abstract class KindOf2<T, TPrimitive>
        where TPrimitive : KindOf2<T, TPrimitive>, new()
    {
        protected KindOf2()
        {
        }

        public static Result<TPrimitive> Create(T value)
        {
            TPrimitive primitive = new TPrimitive();

            var errors = primitive.Validate(value);
            if (errors.Any())
            {
                return new Result<TPrimitive>.Failure(errors.ToImmutableArray());
            }

            primitive.Value = value;

            return primitive;
        }

        protected virtual IEnumerable<Error> Validate(T value)
        {
            yield break;
        }

        public T Value { get; private set; }
    }

But, the above base class would force the child class to implement a public constructor which goes against the reason we are creating domain primitives in the first place. We also cannot use an Init property, because we would have to do validation in the Init and again would have to throw exceptions because we cannot return anything from the Init.

Other methods including reflection, analyzers or code generators are too much indirection for our goal of non-intrusiveness. I may explore those options in a future post just for fun, but for now let us stick with good old-fashioned straightforward code.

Considering that no matter how much we reduce the validation code, we still need to declare a private constructor on each domain primitive type anyway, it seems futile to try and save two simple lines of code for validation.

Hence, we have to accept the above pattern to reap the benefits of domain primitives.

As usual, go to my DomainPrimitives github repo to look at the code.