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
- Keep analyzers fast: They run on every save
- Use canonical naming: Follow conventions
- Test thoroughly: Include edge cases
- Document diagnostics: Explain reasoning
- 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.