Skip to main content

Generating types with build-in invariant preservation using C# source generators (2 / x) : Composition to form complex types

In the last post about "Generating types with build-in invariant preservation using C# source generators" I mentioned composing instances of Validated<T> in a smart way to form complex types that in turn also perverse their invariants. This post will go more into detail on how to do that.

Validated<T> captures the essence of wetter or not an instance of a type T is in a valid state or not.  Validated<T> has 2 subtypes, namely Valid<T> and Invalid<T>. You will never guess what they represent ... Ok, you did. Valid<T> indeed means that whatever the state an instance of T is in, it is in a valid state. Invalid<T>, ... well I think you get it.

So how do we guarantee the instance of T is in a valid or invalid state? Well, by validating the input that leads to a state change. When programming in a functional style, which we are doing here, we don't want to let the state of an instance of T change at all after it has been created. So the place where we need to validate the input is when the instance will be constructed. When the validation fails, we will return an instance of Invalid<T> which holds one or more validation error messages explaining why the state was deemed invalid. The following code shows the same example (the generated code) as in the previous post:

public sealed partial record Name
{
    public static Validated<Name> Create(string value) =>
        string.IsNullOrEmpty(value)
            ? Invalid<string>("The string may not be null or empty")
            : Valid(value);       

    public string Value { get; }

    private Name(string value)
    {
        Value = value;
    }

    public override string ToString() => Value.ToString();
        
    public static implicit operator String(Name value) => value.Value;
}

Since we can only return an instance of the type the constructor belongs to, a static member is used to validate the input and wrap an instance of the type in a Valid<T> when it is valid and Invalid<T> when the state is invalid. In the example, 2 static convenience methods are used (Valid and Invalid) to make the code a tad easier to read.

So now we have simple value types that protect the invariant, you probably wonder how that works when you want to compose these values into a more complex type? Let's start with a simple domain in which we want to make sure only adults can order an alcoholic drink. To make sure we are dealing with an adult, we want to capture this fact into the type system.

public static Beer Order(Adult adult) {}
The implementation of the function is not really relevant. What is relevant though is that we capture the fact that only adults can order a beer. An adult is of course a person that is over 18 years old. So if we capture this invariant in the type Adult, we are darn sure that we got a valid instance of an Adult and not a Minor for instance that is not allowed to order a beer. 

So we want to make sure that every time we create an instance of an adult, the age is always above 18. To be able to identify this adult, we of course always need at least a first name and the last name. To make the code completely obvious, I will not capture the birthdate, but the age as an integer. I will use the generator I explained in the previous article, to capture these invariants in the type system as well:

[Validated<int, IsGreaterOrEqualTo18>] public partial record Age{ } [Validated<string, IsNotNullOrEmpty>] public partial record FirstName { } [Validated<string, IsNotNullOrEmpty>] public partial record LastName{ }

The Validated attribute indicates the type that is being aliased and the type of Validity<T> that is used to represent the validation rules for the type. The type IsGreaterOrEqualTo18 is shown here for clarity:
public class IsGreaterOrEqualTo18 : Validity<int> { public static Validated<int> Validate(int value) => value < 18 ? Invalid<int>("The value must be greater or equal to 18") : Valid(value); }

The generated code for the Age type will look like this:

public sealed partial record Age
{
    public static Validated<Age> Create(Integer value)
{ var result = from validated in global::Radix.Data.Integer.Validity.IsGreaterOrEqualTo18.Validate(value) select new Age(validated); return result;
}       

    public Integer Value { get; }

    private Age(Integer value)
    {
        Value = value;
    }

    public override string ToString() => Value.ToString();
        
    public static implicit operator Integer(Age value) => value.Value;
}

Now for the Adult type:

public record Adult
{
    private Adult(Age age, FirstName firstName, LastName lastName)
{ Age = age; FirstName = firstName; LastName = lastName ; }
    
    private static Func<Age, FirstName, LastName, Adult> New
        => (age, firstName, lastName)
            => new Adult(age, firstName, lastName);
    
    
    public static Validated<Adult> Create(int age, string firstName, string lastName) =>
Valid(New) .Apply(Age.Create(age)) .Apply(FirstName.Create(firstName)) .Apply(LastName.Create(lastName))

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

Finally, we have reached the plot of this post. As we can see, we have a record type with 3 read-only properties for which we have created the types earlier (Age, FirstName, LastName). Just as these value types, Adult has a public static method/smart constructor that returns an instance of Validated<Adult>. It also has a private static constructor in the form of a Func<T, ...>, which might seem weird-ish, but intentional and I'll explain the reason for this in a follow-up post.

The smart constructor uses the fact that the type Validated<T> forms an applicative functor (as well as a Monad and a Functor)... Now forget this for the moment (I will come back to that in a follow-up post for those who want to know the inner workings) and focus on using this fact and what it can do. It takes the function private static Func<Age, FirstName, LastName, Adult> New which is a function that takes three arguments and wraps it in an instance of Valid<Adult> using the Valid function. On this instance in Valid<T>, the function Apply can be called for each of the arguments of New (age, firstName, lastName). The order is significant. So first we call Apply for the age argument. In order to create an instance of an Age, we need to call the smart constructor Create on the Age type, which returns either a Valid<Age> or an Invalid<Age>. The same goes for calling Apply on for FirstName and LastName. Now the great thing is that if ANY of the smart constructors for Age, FirstName, or LastName return an Invalid<T>, it will aggregate ALL the validation error messages for each invalid instance. The Create method for Adult will return an Invalid<Adult> with all the validation messages contained in it. So if only the age and the last name are invalid it will ignore the first name (as it is valid) and contain the validation error messages for age and last name :("The value must be greater or equal to 18" and "The string may not be null or empty"). Please keep in mind that you can make the error message more specific for your scenario. 
Now, we will write the same code structure for all kinds of complex types like Adult which are composed out of Validated<T> 's... Hmmm..... So I should probably write a generator for this :)

When we create an instance of Adult, we actually get an instance of Validated<Adult>. So how can we work with the wrapped Adult instance? We use pattern matching. Valid<T>  is a record, which comes with a deconstructor out of the box. To me the clearest way is to use a switch statement/expression:

switch (validatedAdult) {     case Valid<Adult> (var adult):
        var beer = Order(adult);
        break;     case Invalid<Adult> error: // handle the invalid case. error has a property Reasons that contains all validation error
        // message break; }




Comments

Popular posts from this blog

Running Microsoft Playwright in an Azure Function using C#

When you have tried to run MS Playwright using C# in the context of an Azure Function, you probably have run into this message: The driver it is referring to resides in the .playwright folder that is copied to the build output folder. Now, the output folder structure of an Azure Function project is different from most projects in the sense that there is an extra nested bin folder where the drivers should actually be copied.  The build target that the Playwright team uses, at the time of writing (version 1.15.4), always copies the folder containing the driver to the root of the output folder. So the fix here is to add an extra build target to your project file, the corrects for the extra nested folder:   <Target Name="FixPlaywrightCopyAfterBuild" AfterTargets="Build">     <ItemGroup>       <_BuildCopyItems Include="$(OutDir).playwright\**" />     </ItemGroup>     <Message Text="[Fix] Copying files to the nested bin folder o

Model View Update (MVU) pattern using ASP .NET components and Blazor

A primer on Model View Update (MVU) (UPDATE: The MVU library will be available via  Blazique/Blazique (github.com)  soon. The projects will be removed from Radix with the next major release after the move has been completed). The Model-View-Update (MVU) architecture is a sophisticated blueprint for designing interactive programs, including web applications and games. This architecture is characterized by a unidirectional data flow and is composed of three integral components: the Model, the View, and the Update. The Model is the heart of your application, encapsulating its state. The model preferably should be represented by an immutable data type. The View function is tasked with creating the visual representation of your application. It is a pure function that takes the current model as input and returns a user interface description, such as HTML elements or graphics. The View function refrains from performing any side effects like mut

Type aliases using C# Source Generators and C# 10 generic attributes

When practicing domain-driven design, a reoccurring chore is the creation of value types. These are strongly typed representations of simple types like strings that have a specific meaning, like CustomerId or ProductCode. These could both be strings but we put preferably implement them as strongly typed variations so that we can't for instance mix up multiple string parameters while coding. In some languages, this is something that comes out of the box and is often referred to as type aliasing. C# does not support this. Although in C# you could give another name to a string type with a using statement, it still remains a string (the type does not change).  This task is so common and tedious that it makes it the perfect case for implementing a source generator. Creating a type alias should be as simple as adding an attribute indicating what type should be aliased. It should also work for all kinds of types, like class, record, struct, and record struct. The full source code can be f