Domain Primitives III: easily creating domain-primitive-based data structures from raw data

TL;DR: Only a couple dozen lines of helper code enable easy construction of validated data structures out of raw data as well as proper error tracking.

Looking back at the previous post, we now have a way to force developers to handle errors when creating domain primitives. However, when we use this pattern alone to create many domain primitives or even build data structures out of them, it would be very annoying to have to pattern-match or switch on every single domain primitive creation result.
This post will introduce a way to conveniently work with the established creation pattern.

As an example data structure for this post, I will use a person with a name and email address, where the name itself consists of first and last name. I am well aware that the concept of first name and last name is not universal, can lead to problems in international applications and is one of the classic mistakes when modelling identity, but it is also familiar enough to be used for an example data structure in this post, so, sue me ;-).

We will start (with non-essential differences from the previous post) with this creation pattern for domain primitives:

    public sealed class FirstName : KindOf<string>
    {
        public static Result<FirstName> Create(string value)
        {
            if (value.Equals("Homer", StringComparison.CurrentCultureIgnoreCase))
            {
                return "no Homers allowed";
            }

            return new FirstName(value);
        }

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

    public sealed class LastName : KindOf<string>
    {
        public static Result<LastName> Create(string value) => new LastName(value);

        LastName(string value) : base(value) { }
    }

    public sealed class EMail : KindOf<string>
    {
        public static Result<EMail> Create(string value)
        {
            if (!value.Contains("@"))
            {
                return $"email format is wrong";
            }

            return new EMail(value);
        }

        EMail(string value) : base(value) { }
    }

Using this supporting result type

    public abstract record Result<T>
    {
        Result() { }

        public sealed record Ok(T Item) : Result<T>;
        public sealed record Failure(ImmutableArray<Error> Errors) : Result<T>;

        public static implicit operator Result<T>(T Item) => new Ok(Item);
        public static implicit operator Result<T>(string errorMessage) => new Failure(ImmutableArray.Create<Error>(errorMessage));
    }

    public record Error(string Message, ImmutableArray<Error> ChildErrors)
    {
        public static implicit operator Error(string errorMessage) => new(errorMessage, ImmutableArray<Error>.Empty);
    }

We also define Name and Person types that are composites of our domain primitives. They follow the same creation pattern and validate some made-up consistency constraints. However, being no primitives, they do not inherit from KindOf<T>. Be aware that we cannot easily use the syntax sugar of record types here because we still need to have a private constructor and have to prevent with syntax from circumventing validation. This means declaring the constructor and properties just like we would in a class. The record gives us value semantics though, which is appropriate for Name, but probably not for Person.

    public sealed record Name
    {
        public static Result<Name> Create(FirstName firstName, LastName lastName)
        {
            if (firstName.Value == "John" && lastName.Value == "Doe")
            {
                return "John Doe is a blacklisted name";
            }

            return new Name(firstName, lastName);
        }

        Name(FirstName firstName, LastName lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }

        public FirstName FirstName { get; }
        public LastName LastName { get; }
    }

    public sealed class Person
    {
        public static Result<Person> Create(Name name, EMail eMail)
        {
            if (!eMail.Value.Contains(name.LastName))
            {
                return "email must contain last name";
            }

            return new Person(name, eMail);
        }

        Person(Name name, EMail eMail)
        {
            Name = name;
            EMail = eMail;
        }

        public Name Name { get; }
        public EMail EMail { get; }
    }

Now, how to create a valid person? Without some helper code we would have to match five results to Ok or Failure. This would already be annoying. We also would like to report some kind of error tree to the user in case we did not succeed in creation our person from the data given to us. Time to write some helper code with the following goals:

  • creation of data structures should be possible with syntax resembling the data structure itself, i. e. as some nested expression
  • the result should in the error case contain a useful aggregated error tree
  • syntax should be readable and C#-like, we should not alienate C# developers with functional concepts like functors, map, bind etc.
  • syntax should be as concise as possible
  • of course, everything has to be type-safe

Note that we only care about the creation syntax, but do not really care how the implementation of those requirements will look like. Its functionality is generic enough to be written once and then never looked at again. Hence, it does not matter how much and not even how complicated it is.

Having said that, the solution I will present here happens to be neither complicated nor complex, which proves my rule-of-thumb that most of the time, software does not need complex concepts. Sufficiently abstract and thus simple, but powerful concepts will do just fine. We just have to find the right ones and compose them right.

The solution will enable the following syntax for creation of a person:

Result<Person> personResult = Create.From(Person.Create,
    Create.From(Name.Create,
        FirstName.Create("John"),
        LastName.Create("Miller")),
    EMail.Create("john@miller.com"));

This syntax fulfills the requirements very well. The only syntactic elements used are simple method calls and function parameters written with method group syntax. Also, there is very little cruft. Almost every syntax element carries meaning, with Create.From being the only exception. I would be hard-pressed to find a better way of declaring this kind of composite creation.

The helper code necessary is only a few dozen lines and surprisingly simple:

    public static class Create
    {
        public static Result<TResult> From<TIn0, TIn1, TResult>(
            Func<TIn0, TIn1, Result<TResult>> create,
            Result<TIn0> arg0Result,
            Result<TIn1> arg1Result)
        {
            if (arg0Result is Result<TIn0>.Ok { Item: var arg0 } &&
                arg1Result is Result<TIn1>.Ok { Item: var arg1 })
            {
                return create(arg0, arg1);
            }

            var parameters = create.Method.GetParameters();
            List<Error> errors = new();

            AddErrors(arg0Result, parameters[0], errors);
            AddErrors(arg1Result, parameters[1], errors);

            return new Result<TResult>.Failure(errors.ToImmutableArray());
        }

        static void AddErrors<T>(Result<T> result, ParameterInfo parameterInfo, ICollection<Error> errors)
        {
            if (result is Result<T>.Failure { Errors: var resultErrors })
            {
                errors.Add(new($"parameter '{parameterInfo.Name}' of type {parameterInfo.ParameterType.Name} was not supplied", resultErrors));
            }
        }
    }

For our example, we only need a From method with two input type parameters. It is trivial to bang out / generate more From methods for any number of TResult‘s creation parameters. C# does not let us generalize on the number of type parameters, so we have to live with having 8 or so From methods defined. The standard libraries do similar things for up to 16 type parameters, so meh. As I said, it is write-once code that will not carry maintenance cost. Even generating the methods from a template it is probably not worth it. Garnish with [DebuggerStepThrough], [MethodImpl(MethodImplOptions.AggressiveInlining)] etc. as needed.

In the case of a validation error we even get the nice error tree that we wanted:

If we add a bit of [DebuggerDisplay] niceness to our Result types, we even get a half-decent mouse-over:

If debugging experience is important to us, we can do even better of course and print, say, the first root cause. But as YMMV, that is left as an exercise to the reader ;-).

In conclusion, with just our simple creation pattern and a couple dozen lines of simple write-once helper code, we can avoid accidentally using unvalidated data at compile-time. What amazes me is how little is necessary and how unintrusive the little that is necessary is. It really is just a pattern and some static code. It is not a framework or architecture to buy into, to take all or nothing. It does not force you to do anything. You can use it where you think it makes sense, nowhere or everywhere, it does not matter.

Of course, this post just suggests a foundation on which to build code that has one thing less to worry about. There is still lots of room for improvement. Instead of building it into an error string, we could save the parameter name and type of an unsupplied parameter in a structured way in our Error type. We would also maybe like to enforce our creation pattern (especially the easy-to-forget private constructor) with a code analyzer. The sky is the limit.

BTW, I am currently actually using the techniques from these posts in a real-world project for the first time now. I am excited on how it will go and what downsides and weaknesses will be uncovered. We can only learn 🙂