In the world of software development, efficiency and productivity are key. C# has introduced a powerful feature called Source Generators that can significantly enhance developer productivity by automating repetitive tasks.
In this blog, we’ll explore what Source Generators are, their history, why they are important, and how they can be used with a practical example.
What is a Source Generator in C#?
A Source Generator is a feature in C# that allows developers to automatically generate additional C# code during compilation. It is part of the Roslyn compiler platform and enables you to write code that inspects your project and generates new code based on that analysis.
Key Features of Source Generators:
Compile-Time Code Generation: Code is generated during compilation, not at runtime.
No Runtime Overhead: Since the code is generated at compile time, there is no performance penalty at runtime.
Source Generators are particularly useful for tasks like:
Automatically implementing boilerplate code (e.g., ToString, Equals, GetHashCode).
Generating serialization/deserialization logic.
Creating strongly-typed wrappers for APIs or configurations.
Custom model or DTO class etc.
History of Source Generators in .NET
Source Generators were introduced in .NET 5 as part of the Roslyn compiler. Here’s a brief timeline:
.NET 5 (2020): Source Generators were officially released, allowing developers to generate C# code during compilation.
.NET 6 (2021): Improvements were made to Source Generators, including better performance and tooling support.
.NET 7 (2022): Further enhancements were introduced, such as incremental generators for better performance and reduced memory usage.
Source Generators have become a cornerstone of modern C# development, enabling developers to write less repetitive code and focus on solving business problems.
Why Are Source Generators?
Source Generators are a game-changer for developers. Here’s why they are important:
Boosts Productivity
Automates Repetitive Tasks: Developers no longer need to manually write boilerplate code (e.g., ToString, Equals, etc.).
Reduces Errors: Automatically generated code is consistent and less prone to human error.
Improves Code Maintainability
Keeps Code DRY (Don’t Repeat Yourself): Generated code ensures consistency across the codebase.
Encapsulates Logic: Source Generators encapsulate complex logic, making the main codebase cleaner and easier to understand.
Enhances Performance
Compile-Time Generation: Since code is generated at compile time, there is no runtime overhead.
Incremental Generators: Introduced in .NET 6, these ensure that only the necessary code is regenerated, improving performance.
Example: Generating a ToString Method
Overview:
The ToStringGenerator is a C# Source Generator that automatically generates a ToString method for classes marked with a custom attribute (GenerateToString). The generated ToString method concatenates the values of all public properties in the class.
Key Components
IIncrementalGenerator
The generator implements IIncrementalGenerator, which is part of the Roslyn API for creating source generators.
It allows incremental generation, meaning it only regenerates code when necessary, improving performance.
Initialize Method
This is the entry point for the generator.
It sets up the pipeline for identifying classes with the GenerateToString attribute and generating the ToString method.
Step-by-Step Breakdown
Create a C# class library project and add required dependencies
Create a ToStringGenerator named public class
Identify Target Classes
The generator uses a syntax provider to find classes in the codebase.
The IsSyntaxTarget method filters classes that have at least one attribute (AttributeLists.Count > 0)
private static bool IsSyntaxTarget(SyntaxNode node) { return node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0; }
Filter Classes with GenerateToString Attribute
The GetSemanticTarget method checks if a class has the GenerateToString attribute.
It iterates through the attributes of the class and looks for GenerateToString or GenerateToStringAttribute.
private static ClassDeclarationSyntax? GetSemanticTarget( GeneratorSyntaxContext context) { var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; foreach (var attributeListSyntax in classDeclarationSyntax.AttributeLists) { foreach (var attributeSyntax in attributeListSyntax.Attributes) { var attributeName = attributeSyntax.Name.ToString(); if (attributeName == "GenerateToString" || attributeName == "GenerateToStringAttribute") { return classDeclarationSyntax; } } } return null; }
Register the Source Generator: The Initialize method registers the generator pipeline:
It uses CreateSyntaxProvider to find classes with the GenerateToString attribute.
It filters out null targets and registers the Execute method to generate the ToString method.
public void Initialize(IncrementalGeneratorInitializationContext context) { var classes = context.SyntaxProvider.CreateSyntaxProvider( predicate: static (node, _) => IsSyntaxTarget(node), transform: static (ctx, _) => GetSemanticTarget(ctx)) .Where(static (target) => target is not null); context.RegisterSourceOutput(classes, static (ctx, source) => Execute(ctx, source!)); context.RegisterPostInitializationOutput( static (ctx) => PostInitializationOutput(ctx)); }
Generate the GenerateToString Attribute
The PostInitializationOutput method generates the
This ensures the attribute is available for use in the code.
private static void PostInitializationOutput( IncrementalGeneratorPostInitializationContext context) { context.AddSource("SourceGen.Generators.GenerateToStringAttribute.g.cs", @"namespace SourceGen.Generators { internal class GenerateToStringAttribute : System.Attribute { } }"); }
Generate the ToString Method
The Execute method generates the ToString method for each target class.
It uses a StringBuilder to construct the source code for the ToString method.
The method concatenates the values of all public properties in the class.
private static void Execute(SourceProductionContext context, ClassDeclarationSyntax classDeclarationSyntax) { if (classDeclarationSyntax.Parent is BaseNamespaceDeclarationSyntax namespaceDeclarationSyntax) { var namespaceName = namespaceDeclarationSyntax.Name.ToString(); var className = classDeclarationSyntax.Identifier.Text; var fileName = $"{namespaceName}.{className}.g.cs"; var stringBuilder = new StringBuilder(); stringBuilder.Append($@" namespace {namespaceName} {{ partial class {className} {{ public override string ToString() {{ return $"""); var first = true; foreach (var memberDeclarationSyntax in classDeclarationSyntax.Members) { if (memberDeclarationSyntax is PropertyDeclarationSyntax propertyDeclarationSyntax && propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword)) { if (first) { first = false; } else { stringBuilder.Append("; "); } var propertyName = propertyDeclarationSyntax.Identifier.Text; stringBuilder.Append($"{propertyName}:{{{propertyName}}}"); } } stringBuilder.Append($@"""; }} }} }}"); context.AddSource(fileName, stringBuilder.ToString()); } }
Create a new console app and add project reference to SourceGen.Generators.
Add a partial class Customer with First Name and Last Name properties. As well as GenerateToString attribute on it.
using SourceGen.Generators; namespace SourceGen.ConsoleApp; [GenerateToString] public partial class Customer { public string? FirstName { get; set; } public string? LastName { get; set; } }
Create a customer object and call ToString method—
using SourceGen.ConsoleApp;
var customer = new Customer
{
FirstName = "Khairul",
LastName = "Taher"
};
var customerAsString = customer.ToString();
Console.WriteLine(customerAsString);
Console.ReadLine();
Now add new property middle name in Customer class and update customer object then run the project again—
using SourceGen.Generators; namespace SourceGen.ConsoleApp; [GenerateToString] public partial class Customer { public string? FirstName { get; set; } public string? MiddleName { get; set; } public string? LastName { get; set; } }
using SourceGen.ConsoleApp;
var customer = new Customer
{
FirstName = "Khairul",
MiddleName = "Alam",
LastName = "Taher"
};
var customerAsString = customer.ToString();
Console.WriteLine(customerAsString);
Console.ReadLine();
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Text;
namespace SourceGen.Generators;
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classes = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => IsSyntaxTarget(node),
transform: static (ctx, _) => GetSemanticTarget(ctx))
.Where(static (target) => target is not null);
context.RegisterSourceOutput(classes,
static (ctx, source) => Execute(ctx, source!));
context.RegisterPostInitializationOutput(
static (ctx) => PostInitializationOutput(ctx));
}
private static bool IsSyntaxTarget(SyntaxNode node)
{
return node is ClassDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.AttributeLists.Count > 0;
}
private static ClassDeclarationSyntax? GetSemanticTarget(
GeneratorSyntaxContext context)
{
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
foreach (var attributeListSyntax in classDeclarationSyntax.AttributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
var attributeName = attributeSyntax.Name.ToString();
if (attributeName == "GenerateToString"
|| attributeName == "GenerateToStringAttribute")
{
return classDeclarationSyntax;
}
}
}
return null;
}
private static void PostInitializationOutput(
IncrementalGeneratorPostInitializationContext context)
{
context.AddSource("SourceGen.Generators.GenerateToStringAttribute.g.cs",
@"namespace SourceGen.Generators
{
internal class GenerateToStringAttribute : System.Attribute { }
}");
}
private static void Execute(SourceProductionContext context,
ClassDeclarationSyntax classDeclarationSyntax)
{
if (classDeclarationSyntax.Parent
is BaseNamespaceDeclarationSyntax namespaceDeclarationSyntax)
{
var namespaceName = namespaceDeclarationSyntax.Name.ToString();
var className = classDeclarationSyntax.Identifier.Text;
var fileName = $"{namespaceName}.{className}.g.cs";
var stringBuilder = new StringBuilder();
stringBuilder.Append($@"
namespace {namespaceName}
{{
partial class {className}
{{
public override string ToString()
{{
return $""");
var first = true;
foreach (var memberDeclarationSyntax in classDeclarationSyntax.Members)
{
if (memberDeclarationSyntax
is PropertyDeclarationSyntax propertyDeclarationSyntax
&& propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword))
{
if (first)
{
first = false;
}
else
{
stringBuilder.Append("; ");
}
var propertyName = propertyDeclarationSyntax.Identifier.Text;
stringBuilder.Append($"{propertyName}:{{{propertyName}}}");
}
}
stringBuilder.Append($@""";
}}
}}
}}");
context.AddSource(fileName, stringBuilder.ToString());
}
}
}
Example: Create Model and DTOs referencing database tables
Suppose You have your database CodeGen with two tables. For a new project You want to create all Model and DTOs by referencing database tables—
img
- First of all, read the schema information from the database then map those to TableInfo and ColumnInfo class—
using System.Text; namespace CodeGen; using Microsoft.Data.SqlClient; using System.Collections.Generic; public class DatabaseSchemaScanner { private string _connectionString; public DatabaseSchemaScanner(string connectionString) { _connectionString = connectionString; } public List<TableInfo> GetTables() { List<TableInfo> tables = new List<TableInfo>(); using (SqlConnection connection = new SqlConnection(_connectionString)) { connection.Open(); SqlCommand command = new SqlCommand("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", connection); SqlDataReader reader = command.ExecuteReader(); while (reader.Read()) { string tableName = reader.GetString(0); tables.Add(new TableInfo { Name = tableName, Columns = GetColumns(tableName, connection) }); } } return tables; } private List<ColumnInfo> GetColumns(string tableName, SqlConnection connection) { List<ColumnInfo> columns = new List<ColumnInfo>(); SqlCommand command = new SqlCommand($@" SELECT TABLE_SCHEMA ,TABLE_NAME ,COLUMN_NAME ,CAST(ORDINAL_POSITION as nvarchar(10)) ORDINAL_POSITION ,ISNULL(COLUMN_DEFAULT, '') COLUMN_DEFAULT ,ic.IS_NULLABLE ,DATA_TYPE ,ISNULL(CAST(CHARACTER_MAXIMUM_LENGTH as nvarchar(10)), '') CHARACTER_MAXIMUM_LENGTH ,ISNULL(CAST(NUMERIC_PRECISION as nvarchar(10)), '') NUMERIC_PRECISION ,CAST(is_identity as nvarchar(10)) IsIdentity FROM INFORMATION_SCHEMA.COLUMNS ic INNER JOIN sys.objects so ON ic.TABLE_NAME = so.name INNER JOIN sys.columns sc on so.object_id = sc.object_id AND ic.COLUMN_NAME = sc.name WHERE TABLE_NAME = '{tableName}' ORDER BY ORDINAL_POSITION ASC", connection); SqlDataReader reader = command.ExecuteReader(); while (reader.Read()) { columns.Add(new ColumnInfo { SchemaName = reader.GetString(0), TableName = reader.GetString(1), ColumnName = reader.GetString(2), OrdinalPosition = reader.GetString(3), ColumnDefault = reader.GetString(4), IsNullable = reader.GetString(5), DataType = reader.GetString(6), CharecterMaxLength = reader.GetString(7), NumericPrecision = reader.GetString(8), IsIdentity = reader.GetString(9), }); } return columns; } } public class TableInfo { public string Name { get; set; } public List<ColumnInfo> Columns { get; set; } } public class ColumnInfo { public string SchemaName { get; set; } public string TableName { get; set; } public string ColumnName { get; set; } public string OrdinalPosition { get; set; } public string ColumnDefault { get; set; } public string IsNullable { get; set; } public string DataType { get; set; } public string CharecterMaxLength { get; set; } public string NumericPrecision { get; set; } public string IsIdentity { get; set; } }
Now create GenerateEntityClass function that take TableInfo as param and using SyntaxFactory create the model class step by step —
static void GenerateEntityClass(TableInfo table, string basePath, string projectName) { string domainPath = Path.Combine(basePath, "src", $"{projectName}.Domain", "Entities"); Directory.CreateDirectory(domainPath); // Create class declaration var classDeclaration = SyntaxFactory.ClassDeclaration(table.Name) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); // Add properties with attributes foreach (var column in table.Columns) { // Create property declaration var dataType = column.DataType.ToLower() switch { "int" => "int", "nvarchar" => "string",// Remove single quotes for strings "decimal" => "decimal", "bool" => "bool", _ => "string" }; // Property declaration var propertyDeclaration = SyntaxFactory.PropertyDeclaration( SyntaxFactory.ParseTypeName(dataType), SyntaxFactory.Identifier(column.ColumnName)) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))); // Add [Key] attribute for the first column (assuming it's the primary key) if (column.OrdinalPosition == "1") { propertyDeclaration = propertyDeclaration.AddAttributeLists( SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Key"))))); } // Add [Required] attribute for non-nullable columns if (column.IsNullable == "NO") { propertyDeclaration = propertyDeclaration.AddAttributeLists( SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Required"))))); } // Add [MaxLength] attribute for columns with character maximum length if (!string.IsNullOrEmpty(column.CharecterMaxLength)) { propertyDeclaration = propertyDeclaration.AddAttributeLists( SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("MaxLength")) .AddArgumentListArguments( SyntaxFactory.AttributeArgument( SyntaxFactory.LiteralExpression( SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(int.Parse(column.CharecterMaxLength)))))))); } // Add [DefaultValue] attribute if ColumnDefault is provided if (!string.IsNullOrEmpty(column.ColumnDefault)) { // Parse the default value based on the data type var defaultValue = column.DataType.ToLower() switch { "int" => SyntaxFactory.LiteralExpression( SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(int.Parse(column.ColumnDefault))), "nvarchar" => SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(column.ColumnDefault.Trim('\''))), // Remove single quotes for strings "decimal" => SyntaxFactory.LiteralExpression( SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(decimal.Parse(column.ColumnDefault))), "bool" => SyntaxFactory.LiteralExpression( SyntaxKind.TrueLiteralExpression), // Assuming "1" or "true" for boolean _ => SyntaxFactory.LiteralExpression( SyntaxKind.DefaultLiteralExpression) }; propertyDeclaration = propertyDeclaration.AddAttributeLists( SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DefaultValue")) .AddArgumentListArguments( SyntaxFactory.AttributeArgument(defaultValue))))); } // Add [DatabaseGenerated(DatabaseGeneratedOption.Identity)] if IsIdentity is "1" if (column.IsIdentity == "1") { propertyDeclaration = propertyDeclaration.AddAttributeLists( SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DatabaseGenerated")) .AddArgumentListArguments( SyntaxFactory.AttributeArgument( SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("DatabaseGeneratedOption"), SyntaxFactory.IdentifierName("Identity"))))))); } // Add the property to the class classDeclaration = classDeclaration.AddMembers(propertyDeclaration); } // Create namespace declaration var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName($"{projectName}.Domain.Entities")) .AddMembers(classDeclaration); // Create compilation unit var compilationUnit = SyntaxFactory.CompilationUnit() .AddUsings( SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.ComponentModel.DataAnnotations")), SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.ComponentModel.DataAnnotations.Schema")), SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.ComponentModel"))) // For DefaultValue .AddMembers(namespaceDeclaration); // Write the file File.WriteAllText(Path.Combine(domainPath, $"{table.Name}.cs"), compilationUnit.NormalizeWhitespace().ToFullString()); }
Call the GenerateEntityClass from GenerateCodeFromSchema function for each table—
static void GenerateCodeFromSchema(List<TableInfo> tables, string basePath, string projectName) { foreach (var table in tables) { GenerateEntityClass(table, basePath, projectName); GenerateDtoClass(table, basePath, projectName); GenerateCommandAndQueryClasses(table, basePath, projectName); GenerateValidatorClass(table, basePath, projectName); GenerateControllerClass(table, basePath, projectName); // Generate Tests GenerateUnitTests(table, basePath, projectName); GenerateIntegrationTests(table, basePath, projectName); } GenerateDbContextClass(tables, basePath, projectName); GenerateProgramFile(basePath, projectName); }
It is time to call the GenerateCodeFromSchema from main of program class and see the magic—
static void Main(string[] args) { //Console.WriteLine("Enter project name:"); string projectName = "TstCodegen";//Console.ReadLine(); string connectionString = "Server=CTS-VIVASOFT;Database=CodeGen;Persist Security Info=True;User ID=sa;Password=sa123;MultipleActiveResultSets=true;TrustServerCertificate=true;Connection Timeout=120";//Console.ReadLine(); DatabaseSchemaScanner databaseSchemaScanner = new DatabaseSchemaScanner(connectionString); List<TableInfo> tables = databaseSchemaScanner.GetTables(); // Create project directory string basePath = Path.Combine(Directory.GetCurrentDirectory(), projectName); Directory.CreateDirectory(basePath); // Generate code based on the database schema GenerateCodeFromSchema(tables, basePath, projectName); // Generate .csproj files GenerateCsprojFiles(basePath, projectName); // Generate solution file GenerateSolutionFile(basePath, projectName); Console.WriteLine($"Project '{projectName}' generated successfully at {basePath}"); }
Got to \bin\Debug\net8.0\[name your project]
Run this new generated project and see all of the model and available into the mapped directory—
Source Generators in C# are a powerful tool for automating repetitive tasks, improving code maintainability, and boosting developer productivity. By generating code at compile time, they eliminate the need for manual boilerplate code and ensure consistency across your code base.
In this blog, we explored what Source Generators are, their history, why they are important, and how to use them with a practical example. Whether you’re generating ToString methods, serialization logic, or API wrappers, Source Generators can save you time and effort, allowing you to focus on solving real-world problems.