Isaac.

csharp

Roslyn Advanced Code Generation

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

By Emem IsaacAugust 4, 20243 min read
#roslyn#code generation#analyzers#metaprogramming#csharp
Share:

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.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours