C# > Source Generators > Using Roslyn for Code Generation > Real-World Use Cases
Generating Dapper Mapping Classes
This example demonstrates using a source generator to automatically generate Dapper mapping classes based on database table definitions. This avoids repetitive coding and ensures consistency between database schema and C# code. This could be used in a real world scenario where the application has multiple DTOs and simplifies the boilerplate.
Problem: Boilerplate Dapper Mapping
When using Dapper, mapping database tables to C# classes often involves writing repetitive code to define property mappings. This becomes tedious and error-prone, especially with numerous tables and columns.
Solution: Source Generator for Mapping
A source generator can analyze database schema information (e.g., connection string, table names) and automatically generate the necessary mapping classes. This eliminates manual coding and ensures the mapping is always synchronized with the database schema.
Code: Defining the Attribute
This attribute, `GenerateDapperMappingAttribute`, is used to mark the DTO classes for which we want to generate Dapper mapping extensions. It specifies the database table name and connection string name needed for the generator.
// Define an attribute to mark classes for Dapper mapping generation
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
public class GenerateDapperMappingAttribute : System.Attribute
{
public string TableName { get; set; }
public string ConnectionStringName { get; set; }
public GenerateDapperMappingAttribute(string tableName, string connectionStringName)
{
TableName = tableName;
ConnectionStringName = connectionStringName;
}
}
Code: Defining the Source Generator
This is the core of the source generator. It finds all classes decorated with the `GenerateDapperMappingAttribute`. For each marked class, it extracts the table name and connection string (although in this simplified example we're simulating database access and just using hardcoded column names). Then, it generates a Dapper extension method to retrieve data from the table and map it to the class. The `context.AddSource` method adds the generated code to the compilation.
// Source generator implementation
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using System.Linq;
[Generator]
public class DapperMappingGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Find all classes decorated with the GenerateDapperMappingAttribute
var classSymbols = context.Compilation.SyntaxTrees
.SelectMany(tree => tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
.Select(cds => context.Compilation.GetSemanticModel(cds.SyntaxTree).GetDeclaredSymbol(cds) as INamedTypeSymbol)
.Where(ns => ns != null && ns.GetAttributes().Any(attr => attr.AttributeClass?.Name == "GenerateDapperMappingAttribute"))
.ToList();
foreach (var classSymbol in classSymbols)
{
var attributeData = classSymbol.GetAttributes().First(attr => attr.AttributeClass?.Name == "GenerateDapperMappingAttribute");
string tableName = attributeData.ConstructorArguments[0].Value?.ToString();
string connectionStringName = attributeData.ConstructorArguments[1].Value?.ToString();
if (string.IsNullOrEmpty(tableName) || string.IsNullOrEmpty(connectionStringName))
{
// Add a diagnostic message if the table name or connection string is missing
context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("DAP001", "Missing Configuration", "Table name or Connection string missing for {0}", "DapperMappingGenerator", DiagnosticSeverity.Warning, isEnabledByDefault: true), classSymbol.Locations.FirstOrDefault()));
continue;
}
// Simulate fetching column names from the database
var columnNames = new[] { "Id", "Name", "Description" }; // Replace with actual DB call
// Generate the mapping extension
string sourceCode = GenerateMappingExtension(classSymbol, tableName, columnNames);
context.AddSource($"{classSymbol.Name}_DapperMapping.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}
public void Initialize(GeneratorInitializationContext context) { }
private string GenerateMappingExtension(INamedTypeSymbol classSymbol, string tableName, string[] columnNames)
{
string className = classSymbol.Name;
string namespaceName = classSymbol.ContainingNamespace.ToString();
StringBuilder sb = new StringBuilder();
sb.AppendLine("using Dapper;");
sb.AppendLine("using System.Data;");
sb.AppendLine("using System.Data.SqlClient;");
sb.AppendLine($"namespace {namespaceName}");
sb.AppendLine("{");
sb.AppendLine($" public static class {className}DapperExtensions");
sb.AppendLine(" {");
sb.AppendLine($" public static {className} Get{className}(this IDbConnection connection, int id)");
sb.AppendLine(" {");
sb.AppendLine($" return connection.QuerySingleOrDefault<{className}>($\"SELECT * FROM {tableName} WHERE Id = @Id\", new {{ Id = id }});");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
}
Code: Using the Attribute
This example shows how to use the `GenerateDapperMappingAttribute`. The `MyClass` is decorated with the attribute, specifying the table name "MyTable" and the connection string name "MyConnectionString". During compilation, the source generator will generate a Dapper extension class named `MyClassDapperExtensions` that includes a method `GetMyClass` to retrieve data from the `MyTable` table.
using DapperMappingGenerator;
namespace MyNamespace
{
[GenerateDapperMapping("MyTable", "MyConnectionString")]
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
}
Real-Life Use Case Section
Imagine a large application with hundreds of database tables and corresponding DTOs. Maintaining Dapper mapping code manually would be incredibly time-consuming and prone to errors. This source generator automates the process, significantly reducing development time and ensuring consistency. Also, imagine having the connection string name (or ideally key vault reference) in the annotation for enhanced security.
Best Practices
When to use them
Use source generators when you have repetitive code generation tasks, such as creating mapping classes, data access layers, or implementing design patterns. They are particularly useful when the generated code depends on external information, such as database schema or configuration files.
Interview Tip
When discussing source generators in an interview, be prepared to explain their purpose, how they work, and their benefits. Be able to provide real-world examples of when you would use them. Discuss their advantages over traditional code generation techniques like T4 templates.
Concepts behind the snippet
This snippet utilizes Roslyn's compiler APIs to inspect C# code at compile time. The `ISourceGenerator` interface is implemented to define a custom generator. The `GeneratorExecutionContext` provides access to the compilation information and allows adding generated source code. The `SyntaxTree` and `SemanticModel` are used to analyze the code and extract information. Diagnostic reporting is used to notify the developer of any issues during code generation.
Memory Footprint
Source generators execute during compilation, therefore they don't impact the runtime memory footprint of the application. The generated code itself might have memory implications based on its implementation, but the generator itself does not persist in the running application.
Alternatives
Alternatives to source generators include:
Pros
Cons
FAQ
-
How do I debug a source generator?
You can debug a source generator by attaching a debugger to the `msbuild.exe` process during compilation. Set breakpoints in your source generator code and inspect the variables to understand the code generation process. -
How do I install a source generator?
Source generators are typically distributed as NuGet packages. Add the package to your project, and the source generator will automatically run during compilation. -
Can source generators modify existing code?
No, source generators can only add new code to the compilation. They cannot modify existing source files.