Generating Code in C#

How Source Generators, a New Feature Coming In C# 9.0, Will Help You Automate Code Creation

Abstract

Implementing Equality

public sealed class Person
{
public Person(uint age, string name) =>
(this.Age, this.Name) = (age, name);
public uint Age { get; }
public string Name { get; }
}
public sealed class Person  : IEquatable<Person?>
{
public Person(uint age, string name) =>
(this.Age, this.Name) = (age, name);
public uint Age { get; }
public string Name { get; }
public override bool Equals(object? obj) =>
this.Equals(obj as Person);
public bool Equals(Person? other) =>
other is not null &&
this.Age == other.Age &&
this.Name == other.Name;
public override int GetHashCode() =>
HashCode.Combine(this.Age, this.Name);
public static bool operator ==(Person? left, Person? right) =>
EqualityComparer<Person>.Default.Equals(left, right);
public static bool operator !=(Person? left, Person? right) =>
!(left == right);
}
Using the “Generate Equals and GetHashCode” Visual Studio Refactoring
[Equatable]
public partial sealed class Person
{
public Person(uint age, string name) =>
(this.Age, this.Name) = (age, name);
public uint Age { get; }
public string Name { get; }
}

Implementing ToString()

public override string ToString() =>
$"Age = {this.Age}, Name = {this.Name}";
public static class ObjectExtensions
{
public static string GetString(this object self) =>
string.Join(", ",
self.GetType().GetProperties(
BindingFlags.Instance | BindingFlags.Public)
.Where(_ => _.CanRead)
.Select(_ => $"{_.Name} = {_.GetValue(self)}"));
}
public override string ToString() =>
this.GetString();
[ToString]
public partial class Person { … }

Generating Code

What Are Source Generators?

Creating Object Mappers

public sealed class Source
{
public decimal Amount { get; set; }
public Guid Id { get; set; }
public int Value { get; set; }
public string? Name { get; set; }
}
public sealed class Destination
{
public Guid Id { get; set; }
public int Value { get; set; }
public string? Name { get; set; }
}
var source = new Source
{
Amount = 33M,
Id = Guid.NewGuid(),
Value = 10,
Name = "Woody"
};
var destination = new Destination
{
Id = source.Id,
Value = source.Value,
Name = source.Name
};
[Generator]
public sealed class MapToGenerator
: ISourceGenerator
{
public void Execute(GeneratorExecutionContext context) { ... }
public void Initialize(GeneratorInitializationContext context) { ... }
}
public void Initialize(GeneratorInitializationContext context) =>
context.RegisterForSyntaxNotifications(() => new MapToReceiver());
public sealed class MapToReceiver
: ISyntaxReceiver
{
public List<TypeDeclarationSyntax> Candidates { get; } =
new List<TypeDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if(syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax)
{
foreach (var attributeList in
typeDeclarationSyntax.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
if(attribute.Name.ToString() == "MapTo" ||
attribute.Name.ToString() == "MapToAttribute")
{
this.Candidates.Add(typeDeclarationSyntax);
}
}
}
}
}
}
[MapTo(typeof(Destination))]
public class Source { … }
var (mapToAttributeSymbol, compilation) =
Assembly.GetExecutingAssembly().LoadSymbol(
"InlineMapping.MapToAttribute.cs",
"InlineMapping.MapToAttribute", context);
if (context.SyntaxReceiver is MapToReceiver receiver)
{
foreach (var candidateTypeNode in receiver.Candidates)
{
var model = compilation.GetSemanticModel(
candidateTypeNode.SyntaxTree);
var candidateTypeSymbol = model.GetDeclaredSymbol(
candidateTypeNode) as ITypeSymbol;
if (candidateTypeSymbol is not null)
{
foreach (var mappingAttribute in
candidateTypeSymbol.GetAttributes()
.Where(
_ => _.AttributeClass!.Equals(
mapToAttributeSymbol, SymbolEqualityComparer.Default)))
{
var (diagnostics, name, text) =
MapToGenerator.GenerateMapping(
candidateTypeSymbol, mappingAttribute);
foreach (var diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic);
}
if (name is not null && text is not null)
{
context.AddSource(name, text);
}
}
}
}
}
var diagnostics = ImmutableList.CreateBuilder<Diagnostic>();
var destinationType =
(INamedTypeSymbol)attributeData.ConstructorArguments[0].Value!;
if (!destinationType.Constructors.Any(
_ => _.DeclaredAccessibility == Accessibility.Public &&
_.Parameters.Length == 0))
{
diagnostics.Add(Diagnostic.Create(
new DiagnosticDescriptor(...)));
}
maps.Add(
$"\t\t\t\t\t{destinationProperty.Name} = self.{sourceProperty.Name},");
var source = new Source
{
Amount = 33M,
Id = Guid.NewGuid(),
Value = 10,
Name = "Woody"
};
var destination = source.MapToDestination();Using “Go To Definition” on MapToDestination() shows this:using System;namespace SourceNamespace
{
public static partial class SourceMapToExtensions
{
public static Destination MapToDestination(this Source self) =>
self is null ? throw new ArgumentNullException(nameof(self)) :
new Destination
{
Id = self.Id,
Value = self.Value,
Name = self.Name,
};
}
}

Other Examples

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store