Skip to main content

Generating types with build-in invariant preservation using C# source generators (1 / x) : Simple types

In an earlier post, I described a way for generating value types. In this post, I want to show how to take this a step further. Usually, one does not only want an alias but enforce an invariant using the type system at the same time. 

So first let's recap what we mean by an invariant. Plainly put, an invariant is a condition that stays true no matter what happens. For example, let's suppose we have a value in our system that represents a quantity that must always be greater than zero. That is an invariant.

Invariants pop up all over the place when modeling a domain. We want to enforce these invariants because we want to guard the integrity of our domain model. One very convenient way of guarding the integrity of values within the domain is by capturing invariants directly into the type system. Once you successfully create an instance of a type guarding the invariant, you never have to check its validity again. In addition to that, the validation logic is captured in 1 place, which makes the code very maintainable.  

The most straightforward method of achieving this for the quantity example would be to check a constructor parameter and throw an exception if it were zero. Now, while this is a perfectly acceptable code, it has some disadvantages. For one, you would need to sprinkle try-catch blocks throughout your codebase. Another one is that the semantics of exceptions, in general, is not directly related to invalid values. An exception can be thrown for a multitude of reasons, like IO errors, etc.

I will opt into being explicit about values being valid or invalid by using the types Valid<T> and Invalid<T>. Just as code that has asynchronous effects in C# is explicitly captured in the type Task<T>, the fact that a constructor (or any other function for that matter) can return a valid or invalid result can be captured by the type Validated<T> and its subtypes Valid<T> and Invalid<T>. As we know, a constructor can only return the type it is a member of. So we need a different option to construct an instance. A pattern usually called a smart constructor is the solution for that. This uses a private constructor and a public static method (usually called Create) that does the validation, creates the instance by calling the private constructor and returns the result wrapped in either an instance of Valid<T> or Invalid<T> (the return type being Validated<T>). An example:

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;
}

This ensures we can only create a valid Name when the value is not null or empty. Once we have an instance of a Valid<Name> we never have to check its validity again. The invariant is preserved using the type system. Notice that Value is read-only. This also prevents a copy when using a "with" expression on the record instance to circumvent validation. Later we will see that we can combine/compose instances of Validate<T> in a smart way to form complex types that in turn also perverse their invariants.

The general code construct shown above is similar for most values. So given we are lazy, don't want to make errors by copy, pasting, and replacing, we want to make a source generator for this. How about being able to write something like this:
[Validated<string, IsNotNullOrEmpty>]
public partial record Name { }
The C# 10 generic attribute indicates we want a type Name, which is an alias for a string and may never be null nor empty. It needs to be partial because we are going to generate the rest of the code (the other partial uhm ... well ... part). The definition of the attribute type is as follows:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
public class ValidatedAttribute<T, V> : Attribute where V : Validity<T>{}
T signifies the type we are aliasing. V needs to be of a type Validaty<T>. Validity<T> is an interface (yes, I do not use the I prefix convention.... bite me :-) ) with 1 static abstract member (as of writing, this feature is still in preview...):
public interface Validity<T>
{
    public static abstract Validated<T> Validate(T value);
}
In the above example, V is of type IsNotNullOrEmpty, which is an implementation of Validity<string>:

public class IsNotNullOrEmpty : Validity<string>
{
    public static Validated<string> Validate(string value) =>
        string.IsNullOrEmpty(value)
            ? Invalid<string>("The string may not be null or empty")
            : Valid(value);
}
The validation rules can be as complex as necessary of course, but I kept it simple. The generated code will use the instance of Validity<T> to validate the input and will look something like this:

using static Radix.Validated.Extensions;

public sealed partial record Name
{
    public static Validated<Name> Create(String value)
    {
        var result = from validated in global::Radix.Data.String.Validity.IsNotNullOrEmpty.Validate(value, "The value provided for Name is not valid")
                             select new Name(validated);
        return result;
    }
        
    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;
}

Some things need to be clarified off course. The generated code uses some types, like Radix.Data.String.Validity.IsNotNullOrEmpty, which are generic pieces of code I built as part of the Radix library. Another thing that might have been noticed is that the LINQ syntax is used for the Validated<T> type. This can be done because the Select and SelectMany functions have been implemented for this type (it forms a functor and a monad, just like IEnumerable does). This becomes especially convenient when one wants to combine the value types to form complex types as mentioned earlier.

The code for the actual generator is very similar to the generator used for value types described in a previous post and can be found here. The only differences are that it looks for the ValidatedAttribute and the code generation template :

var source = new StringBuilder($@"
namespace {namespaceName}
{{
    using static Radix.Validated.Extensions;

    {kindSource}
    {{
        public static Validated<{typeSymbol.Name}> Create({valueType} value)
        {{
            var result = from validated in {validityType}.Validate(value, ""The value provided for {typeSymbol.Name} is not valid"")
                         select new {typeSymbol.Name}(validated);
            return result;
        }}
        

        public {valueType} {propertyName} {{ get; }}

        private {typeSymbol.Name}({valueType} value)
        {{
            {propertyName} = value;
        }}

        public override string ToString() => {propertyName}.ToString();

        {equalsSource}

        public static implicit operator {valueType}({typeSymbol.Name} value) => value.{propertyName};
    }}
}}");

In the next post, I will go deeper into how to combine these (generated) validated types into more complex types and how you would use these principles in practice.




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

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

Simple but effective use of C# Source Generators

Most C# source generator examples I have encountered included more advanced features like augmenting existing classes with generated code. There are much simpler scenarios where they are useful though. For instance, just avoiding typing repetitive code. For the full source code, consult my GitHub repo here . Imagine writing a number of overloads where only the function name changes. In this case, I want to create HTML tags as strings using a function. The example is a set of overloads for the anchor ('a') tag: public static Node a(params Node[] nodes) =>      element(nameof(a), Array.Empty<IAttribute>(), nodes); public static Node a(params IAttribute[] attributes) =>      element(nameof(a), attributes, Array.Empty<Node>()); public static Node a(IEnumerable<IAttribute> attributes, params Node[] children) =>      element(nameof(a), attributes, children); Now I want the have the same overloads for all HTML tags. That's a lot of repetition. To cre