Generating Code in C#

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

Abstract

A new feature in .NET 5 that will be available in C# 9.0 (which is slated for release in November 2020) is called “source generators.” It provides a means to create code based on expected conditions in existing code. This feature is baked into the compiler, creating a seamless, native code generation experience. In this article, I’ll dive into source generators and demonstrate how you can use them for your own implementation needs.

Implementing Equality

To start, let’s look at what it takes to implement equality for a type. Consider the simplistic definition of a person shown in Listing 1.

public sealed class Person
{
public Person(uint age, string name) =>
(this.Age, this.Name) = (age, name);
public uint Age { get; }
public string Name { get; }
}
  • You must implement IEquatable<T>
  • You should override the == and != operators
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()

Automating the generation of repeatable code is a desirable feature, but there’s another aspect to generating code, which is to implement performant applications.

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

Generating code isn’t a new concept. Developers use code generators in multiple ways, either through simplistic approaches such as string building or by using existing tools or ones they create. For example, T4 is a tool that leverages a template engine to create code. Scriban is another one. The issue is none of these have native integration with the underlying C# compiler. With the scenarios described in the previous section, changes to the Person type might affect the equality and ToString() implementations. Having the code regenerated automatically reduces any discrepancies and errors. Coupled with good IDE integration, the developer can also see the generated code and immediately understand what’s going on.

What Are Source Generators?

A source generator, as defined by Microsoft, is “a piece of code that runs during compilation and can inspect your program to produce additional files that are compiled together with the rest of your code.”

Creating Object Mappers

The area we’re going to tackle with source generation is object mapping. The idea is simple: Take two objects that may or may not have any kind of type relationship and map the matching property values from one to another. For example, in Listing 5, we have two types, a source type, and a destination type.

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

My InlineMapping example is one example of how you can use source generators in C#. As I mentioned, there’s already a framework out there called AutoMapper that’s extremely popular (at the time I’m writing this article, the package has over 100 million downloads). It’s conceivable that package’s implementation could be changed to use source generators, making it even faster. Plenty of other examples show where the power of source generators come into play:

  • StrongInject — A performant, compile-time checked inversion-of-control (IoC) container
  • ThisAssembly — Exposes assembly information with an easy-to-use interface
  • Rocks — This is my other source generator project, a package that creates mocks for tests. I’m currently changing it so the mocks are generated at compile time. You can follow the work through this issue.

Conclusion

Source generators are a powerful feature coming to C# 9.0. With this feature, you can modernize repetitive coding patterns in a safe, performant manner. I encourage you to give it a try. You may be surprised how much you can accomplish when you generate code. Happy coding!

--

--

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