Isaac.

Roslyn Advanced Code Generation

Generate code at compile-time with Roslyn analyzers and generators.

By EMEPublished: February 20, 2025
roslyncode generationanalyzersmetaprogrammingcsharp

A Simple Analogy

Roslyn is like giving C# the ability to write itself. During compilation, Roslyn analyzes and generates code automatically, eliminating boilerplate.


What Is Roslyn?

Roslyn is .NET's compiler platform, exposing APIs to analyze and transform C# code at compile-time.


Diagnostic Analyzers

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncVoidAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "ASYNC001";
    private static readonly LocalizableString Title = "Avoid async void methods";
    private static readonly LocalizableString MessageFormat = "Method '{0}' is async void. Use Task or Task<T> instead.";
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(new DiagnosticDescriptor(
            DiagnosticId, Title, MessageFormat,
            category: "Usage",
            defaultSeverity: DiagnosticSeverity.Warning,
            isEnabledByDefault: true));
    
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
    }
    
    private static void AnalyzeSymbol(SymbolAnalysisContext context)
    {
        var methodSymbol = context.Symbol as IMethodSymbol;
        
        if (methodSymbol?.IsAsync == true && methodSymbol.ReturnsVoid)
        {
            var diagnostic = Diagnostic.Create(
                SupportedDiagnostics[0],
                methodSymbol.Locations[0],
                methodSymbol.Name);
            
            context.ReportDiagnostic(diagnostic);
        }
    }
}

// Usage: analyzer warns about this
public async void BadMethod()  // ASYNC001: Avoid async void methods
{
    await Task.Delay(1000);
}

// Fix suggested
public async Task GoodMethod()
{
    await Task.Delay(1000);
}

Code Fixes

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public class AsyncVoidCodeFix : CodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds =>
        ImmutableArray.Create(AsyncVoidAnalyzer.DiagnosticId);
    
    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
        var diagnostic = context.Diagnostics[0];
        var diagnosticSpan = diagnostic.Location.SourceSpan;
        
        var method = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
            .OfType<MethodDeclarationSyntax>()
            .First();
        
        context.RegisterCodeFix(
            CodeAction.Create(
                title: "Change return type to Task",
                createChangedDocument: c => FixReturnType(context.Document, method, c),
                equivalenceKey: "fix-async-void"),
            diagnostic);
    }
    
    private async Task<Document> FixReturnType(
        Document document,
        MethodDeclarationSyntax method,
        CancellationToken cancellationToken)
    {
        var root = await document.GetSyntaxRootAsync(cancellationToken);
        var newMethod = method.WithReturnType(
            SyntaxFactory.ParseTypeName("Task"));
        
        var newRoot = root.ReplaceNode(method, newMethod);
        return document.WithSyntaxRoot(newRoot);
    }
}

Syntax Analysis

var code = @"
public class MyClass
{
    public void MyMethod()
    {
        var x = 42;
    }
}";

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitSyntax();

// Find all methods
var methods = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .Select(m => m.Identifier.Text)
    .ToList();

Console.WriteLine($"Methods: {string.Join(", ", methods)}");  // Output: Methods: MyMethod

// Find all variable declarations
var variables = root.DescendantNodes()
    .OfType<VariableDeclaratorSyntax>()
    .Select(v => v.Identifier.Text);

Semantic Analysis

public static class SymbolAnalysis
{
    public static void AnalyzeFile(string filePath)
    {
        var workspace = MSBuildWorkspace.Create();
        var project = workspace.OpenProjectAsync(projectPath: "MyProject.csproj").Result;
        var compilation = project.GetCompilationAsync().Result;
        var tree = compilation.SyntaxTrees.First();
        var root = tree.GetCompilationUnitSyntax();
        
        var semanticModel = compilation.GetSemanticModel(tree);
        
        // Find all method calls
        var invocations = root.DescendantNodes()
            .OfType<InvocationExpressionSyntax>();
        
        foreach (var invocation in invocations)
        {
            var symbolInfo = semanticModel.GetSymbolInfo(invocation.Expression);
            var methodSymbol = symbolInfo.Symbol as IMethodSymbol;
            
            Console.WriteLine($"Calling: {methodSymbol?.ContainingType?.Name}.{methodSymbol?.Name}");
        }
    }
}

Best Practices

  1. Keep analyzers fast: They run on every save
  2. Use canonical naming: Follow conventions
  3. Test thoroughly: Include edge cases
  4. Document diagnostics: Explain reasoning
  5. Version carefully: Changes affect all users

Related Concepts

  • FxCop / Code Analysis
  • StyleCop analyzers
  • NDepend for architecture
  • Compiler APIs

Summary

Roslyn enables compile-time code analysis and generation. Build analyzers to enforce patterns and code fixes to automate remediation.