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(classSource, Encoding.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
Post a Comment