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

  • Handle database connection and schema retrieval carefully. Use a robust mechanism to fetch column names, potentially caching the schema information for performance.
  • Add error handling and logging to the source generator for debugging purposes.
  • Consider supporting different Dapper mapping configurations, such as column name transformations.
  • Test the generated code thoroughly to ensure it works as expected.

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:

  • T4 Templates: These are text templates that can generate code, but they are less integrated with the compiler and can be harder to debug.
  • Reflection: Code can be generated at runtime using reflection, but this can negatively impact performance.
  • Code weaving: Using tools like PostSharp, aspects can be injected into the compiled code, which can be used to implement cross-cutting concerns.

Pros

  • Reduced boilerplate code.
  • Improved code maintainability.
  • Enhanced performance (code is generated at compile time).
  • Compile-time validation of generated code.

Cons

  • Increased complexity of the build process.
  • Steeper learning curve for developers.
  • Debugging source generators can be challenging.

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.