Skip to main content

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 found in my GitHub repo here.

C# 10 introduces a nice new feature that allows one to define generic attributes, so that attribute for marking type aliases can be the following:

[Alias<string>] public partial struct ProductCode { }

The source generator should augment this struct with additional code, so it should be marked as partial. The augmented code should implement equality (when not a record (struct)).

The attribute is nothing more than a marker:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] public class AliasAttribute<T> : Attribute{}

The only thing noteworthy is the fact that it is a generic attribute, which is new to C# 10. In this case, it is especially useful since it provides a convenient, strongly type, way of defining the type to be wrapped/aliased.

The source generator will search for the types annotated by this attribute. The first stage in searching is selecting candidate types (classes, records, and (record) structs) on which any attribute is placed. This is done by examining syntax nodes using an instance of a custom syntax receiver.

internal class SyntaxReceiver : ISyntaxReceiver {     internal IList<TypeDeclarationSyntax> CandidateTypes { get; } =          new List<TypeDeclarationSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)     {         if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax             && classDeclarationSyntax.AttributeLists.Count > 0)             CandidateTypes.Add(classDeclarationSyntax);

        if (syntaxNode is StructDeclarationSyntax structDeclarationSyntax             && structDeclarationSyntax.AttributeLists.Count > 0)             CandidateTypes.Add(structDeclarationSyntax);

        if (syntaxNode is RecordDeclarationSyntax recordDeclarationSyntax             && recordDeclarationSyntax.AttributeLists.Count > 0)             CandidateTypes.Add(recordDeclarationSyntax);     } }

The second phase is filtering out the types that are decorated with the specific AliasAttribute. For each found type, source code will be generated to augment it which will be added to the compilation. Note that the metadata name for generic types includes a backtick followed by the number of generic parameters e.g. "Radix.AliasAttribute`1" 


public void Execute(GeneratorExecutionContext context)
{
    if (context.SyntaxReceiver is not SyntaxReceiver receiver) return;

    foreach (var candidate in receiver.CandidateTypes)
    {
        var model = context.Compilation.GetSemanticModel(candidate.SyntaxTree);
        var typeSymbol = ModelExtensions.GetDeclaredSymbol(model, candidate);
        var attributeSymbol = context.Compilation.GetTypeByMetadataName("Radix.AliasAttribute`1");
        var attributes = typeSymbol.GetAttributes().Where(attribute => attribute.AttributeClass.Name.Equals(attributeSymbol.Name));
        foreach (var attribute in attributes)
        {
            var classSource = ProcessType(attribute.AttributeClass.TypeArguments.First().Name, typeSymbol, attributeSymbol, candidate);
            context.AddSource(
                $"{typeSymbol.ContainingNamespace.ToDisplayString()}_{typeSymbol.Name}_alias",
                SourceText.From(classSourceEncoding.UTF8));
        }
    }
}

When generating the code preferably the code will live in the same namespace as the code it will augment. The namespace can be accessed via the ISymbol reference of the type that needs to be augmented.

var namespaceName = typeSymbol.ContainingNamespace.ToDisplayString();

To create the proper partial type declaration, the kind of the TypeDeclarationSyntax (of the candidate type decorated with the AliasAtrribute that was found in an earlier step) is inspected. For each kind, a different piece of source code is generated. A record (struct) already, by definition, has structural equality, so these kinds do not need to implement IEquatable.

var kindSource = typeDeclarationSyntax.Kind() switch
    {
        SyntaxKind.ClassDeclaration => $"public sealed partial class {typeSymbol.Name}  : System.IEquatable<{typeSymbol.Name}>",
        SyntaxKind.RecordDeclaration => $"public sealed partial record {typeSymbol.Name}",
        SyntaxKind.StructDeclaration => $"public partial struct {typeSymbol.Name}  : System.IEquatable<{typeSymbol.Name}>",
        SyntaxKind.RecordStructDeclaration => $"public partial record struct {typeSymbol.Name} ",
        _ => throw new NotSupportedException("Unsupported type kind for generating Alias code")
    };
Next, the code for the equality and equality operators are created. This will only be added to the kind of types that need explicit equality.

var equalsOperatorsSource = $@"
    public static bool operator ==({typeSymbol.Name} left, {typeSymbol.Name} right) => Equals(left, right);
    public static bool operator !=({typeSymbol.Name} left, {typeSymbol.Name} right) => !Equals(left, right);|
";

var equalsSource = typeDeclarationSyntax.Kind() switch {     SyntaxKind.ClassDeclaration =>         $@"             {equalsOperatorsSource}             public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is {typeSymbol.Name} other && Equals(other);             public override int GetHashCode() => {propertyName}.GetHashCode();             public bool Equals({typeSymbol.Name} other){{ return {propertyName} == other.{propertyName}; }}         ",

    SyntaxKind.RecordDeclaration => "",

    SyntaxKind.StructDeclaration =>         $@"             {equalsOperatorsSource}             public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is {typeSymbol.Name} other && Equals(other);             public override int GetHashCode() => {propertyName}.GetHashCode();             public bool Equals({typeSymbol.Name} other)             {{                 if (ReferenceEquals(null, other)) return false;                 if (ReferenceEquals(this, other)) return true;                 return {propertyName} == other.{propertyName};             }}         ",

    SyntaxKind.RecordStructDeclaration => "",         _ => throw new NotSupportedException("Unsupported type kind for generating Alias code") };

Finally, all is combined. A private constructor is created that is called by the explicit conversion operator, which means for creating an instance of the alias only an explicit cast is needed that adds no /  negligible overhead, while still being explicit about what the type is.

var source = new StringBuilder($@"

namespace {namespaceName} {{     {kindSource}     {{         public {valueType} {propertyName} {{ get; }}                 private {typeSymbol.Name}({valueType} value)         {{             {propertyName} = value;         }}

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

        {equalsSource}

        public static explicit operator {typeSymbol.Name}({valueType} value) => new {typeSymbol.Name}(value);

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

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