Roslyn's phases, immutable syntax trees, Compilation and SemanticModel, IOperation, emit, the compiler server and using the APIs to build tools.
Roslyn is the C# and VB compiler exposed as libraries. It parses source into immutable syntax trees (with full fidelity: every space and comment is kept as trivia), builds declarations and symbols, binds expressions to symbols and types, runs flow analysis, then lowers and emits IL, metadata and PDBs. The same objects are public: SyntaxTree, CSharpCompilation, SemanticModel, ISymbol and IOperation. IDEs, analysers, source generators and refactoring tools are all built on these APIs. You can use them to compile code at run time, write code-mod scripts that rewrite thousands of files, or build analysers that run in every build.
Roslyn is the .NET Compiler Platform: the C# and VB compilers rewritten as libraries with public APIs. Instead of a black box that turns source into DLLs, it exposes every stage — parsing, binding, emitting — so IDEs, analysers, source generators, refactoring tools and your own utilities can use the same understanding of code as the compiler. I'd describe the pipeline and the three core API concepts (syntax trees, semantic model, compilation), then show how I'd use them in a tool.
Source text
→ Parser → SyntaxTree (per file)
→ Declaration → Symbols (types, members) for the whole compilation
→ Binder → Bound trees, SemanticModel (types, overloads, conversions)
→ Flow analysis → definite assignment, nullability, reachability
→ Lowering → simpler constructs (foreach, async, lambdas...)
→ Emitter → IL + metadata + PDB
Analysers run on syntax/semantic information; source generators run after initial parsing and add syntax trees before the final compilation.
var tree = CSharpSyntaxTree.ParseText(File.ReadAllText("OrderService.cs"));
var root = tree.GetCompilationUnitRoot();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
root.ReplaceNode(...)), enabling safe concurrent use and easy refactorings.SyntaxFactory builds new nodes; SyntaxRewriter/CSharpSyntaxWalker traverse/transform.Syntax alone doesn't know what Order refers to or which overload is called.
var references = Directory.GetFiles(Path.GetDirectoryName(typeof(object).Assembly.Location)!, "*.dll")
.Select(p => MetadataReference.CreateFromFile(p));
var compilation = CSharpCompilation.Create("Analysis", [tree], references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable));
var model = compilation.GetSemanticModel(tree);
foreach (var invocation in root.DescendantNodes().OfType<InvocationExpressionSyntax>())
{
if (model.GetSymbolInfo(invocation).Symbol is IMethodSymbol method &&
method.ContainingType.ToDisplayString() == "System.DateTime" && method.Name == "get_Now")
Console.WriteLine($"DateTime.Now at {invocation.GetLocation().GetLineSpan()}");
}
GetDiagnostics()) and emit an assembly (Emit(stream)).GetSymbolInfo (what does this name/call bind to?), GetTypeInfo (type of an expression, converted type), GetDeclaredSymbol, GetConstantValue, data/control-flow analysis.INamedTypeSymbol, IMethodSymbol, ...) represent declarations from source or metadata, with rich info (accessibility, attributes, members, nullability).Microsoft.CodeAnalysis.Workspaces.MSBuild): MSBuildWorkspace.Create().OpenSolutionAsync("App.sln") loads real projects with references, so you can analyse or refactor a whole solution (requires MSBuildLocator.RegisterDefaults()).Newtonsoft attributes to System.Text.Json, update APIs across hundreds of files).SyntaxFactory (or raw strings).Microsoft.CodeAnalysis.CSharp.Scripting) for rules engines (with care).dotnet format/code fixes).Migrating 400 controllers from Newtonsoft.Json attributes and JObject usage to System.Text.Json, we wrote a small Roslyn Workspaces tool: it loaded the solution, used the semantic model to find attribute usages ([JsonProperty("x")] → [JsonPropertyName("x")]) and JsonConvert calls, rewrote nodes with SyntaxFactory while preserving trivia, and reported cases needing manual work. It changed ~3,000 sites in minutes with a reviewable diff, far safer than regex replacement.
The interviewer checks you understand the compiler as a platform, not a black box.
Green flags: names phases and their APIs, immutability and full fidelity, syntax versus semantic model, IOperation, compiler server and reference assemblies, practical tool examples.
Red flags: only knows csc as a command; would use regex for large code changes; confuses syntax and semantics.
Roslyn uses green nodes (immutable, parent-less, shared, cached) internally and red nodes (the public SyntaxNode API with parent pointers and positions) created lazily on top. This gives immutability and cheap incremental reparsing (only changed parts recreated).
tree.WithChangedText(newText) reuses unchanged green nodes — essential for IDE responsiveness.
Symbols are tied to a compilation; compare with SymbolEqualityComparer.Default. For cross-compilation lookups (e.g. in Workspaces after edits), use SymbolFinder and symbol keys.
Solution → Project → Document, all immutable snapshots. Edits create new solutions; workspace.TryApplyChanges(newSolution) writes to disk (MSBuildWorkspace). Formatter.FormatAsync and Simplifier tidy generated syntax.
compilation.Emit(peStream, pdbStream) produces assemblies — used by scripting, testing tools (compile snippets in tests), and dynamic plugin compilation (rarely appropriate in production; security and load-context concerns).
Compilations are expensive to create; reuse them. Semantic models are cached per tree. Use IOperation for less allocation-heavy analysis in some cases. Avoid calling GetDiagnostics() repeatedly on large compilations.
DateTime.Now usages via the semantic model.MSBuildWorkspace to load a solution and rename a method across projects with Renamer.// dotnet add package Microsoft.CodeAnalysis.CSharp
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Emit;
SyntaxTree tree = CSharpSyntaxTree.ParseText("""
public static class Calc
{
public static int Twice(int x) => x * 2;
}
""");
// Reference the same framework assemblies this app runs on
IEnumerable<MetadataReference> references =
((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Select(path => MetadataReference.CreateFromFile(path));
CSharpCompilation compilation = CSharpCompilation.Create(
"Dynamic", [tree], references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
// Semantic question: what symbol is the method?
SemanticModel model = compilation.GetSemanticModel(tree);
MethodDeclarationSyntax method = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>().Single();
IMethodSymbol symbol = model.GetDeclaredSymbol(method)!;
Console.WriteLine($"{symbol.ContainingType.Name}.{symbol.Name} returns {symbol.ReturnType}");
// Emit and run
using var pe = new MemoryStream();
EmitResult result = compilation.Emit(pe);
if (!result.Success)
{
foreach (Diagnostic d in result.Diagnostics) Console.WriteLine(d);
return;
}
Assembly assembly = Assembly.Load(pe.ToArray());
object? value = assembly.GetType("Calc")!.GetMethod("Twice")!.Invoke(null, [21]);
Console.WriteLine(value); // 42
What happens: large refactors with find/replace or regex. Why it is wrong: misses semantics, breaks code, loses formatting. Do instead: Roslyn syntax/semantic rewrites.
What happens: matching method names textually. Why it is wrong: can't distinguish overloads, aliases, extension methods. Do instead: use the semantic model or IOperation.
What happens: creating compilations without framework references. Why it is wrong: symbols unresolved; wrong results. Do instead: add proper references or use Workspaces.
==What happens: reference comparisons of symbols.
Why it is wrong: unreliable across contexts.
Do instead: SymbolEqualityComparer.Default.
What happens: generated nodes lose comments/formatting.
Why it is wrong: noisy diffs and lost comments.
Do instead: preserve trivia (WithTriviaFrom) and format.
MSBuildLocatorWhat happens: Workspaces fail to load projects. Why it is wrong: MSBuild assemblies not found. Do instead: register MSBuild before using workspaces.
What happens: using Roslyn to compile arbitrary scripts at run time. Why it is wrong: security, memory and load-context issues. Do instead: constrained rules engines or sandboxed processes.
What happens: one-off scripts for rules that should be enforced continuously. Why it is wrong: rules drift after the script runs. Do instead: analysers for ongoing enforcement.
Roslyn knowledge turns large, risky changes into automated, reviewable ones.
| Need | Roslyn tool |
|---|---|
| Enforce a rule in every build | Analyser (with code fix) |
| Generate boilerplate at build time | Incremental source generator |
| One-off migration across a repo | Console tool with MSBuildWorkspace and a rewriter |
| Run user-defined rules at run time | Compile to a collectible load context |
Seniors also know the cost: compilations are memory-heavy, so tools should process projects one at a time.
An analyser is a class deriving from DiagnosticAnalyzer with [DiagnosticAnalyzer(LanguageNames.CSharp)]. It declares DiagnosticDescriptors (id, title, message, category, default severity) and in Initialize calls EnableConcurrentExecution(), ConfigureGeneratedCodeAnalysis(...) and registers callbacks such as RegisterSymbolAction, RegisterSyntaxNodeAction or RegisterOperationAction, reporting with context.ReportDiagnostic. It targets netstandard2.0 like a generator. Test it with Microsoft.CodeAnalysis.CSharp.Analyzer.Testing using markup such as {|DEMO001:Run|}. Ship it as a project reference with OutputItemType="Analyzer" or a NuGet package, and let teams tune severity in .editorconfig.
A source generator is a class marked [Generator] that implements IIncrementalGenerator (the older ISourceGenerator is deprecated). It runs inside the compiler, reads the user's code through Roslyn and adds new C# files to the compilation; it cannot modify existing code. The generator project targets netstandard2.0, references Microsoft.CodeAnalysis.CSharp (a version no newer than your consumers' compilers) with PrivateAssets="all", and sets EnforceExtendedAnalyzerRules. Consumers reference it as an analyser (OutputItemType="Analyzer" ReferenceOutputAssembly="false"), or you pack it into analyzers/dotnet/cs in a NuGet package. Use RegisterPostInitializationOutput for marker attributes and EmitCompilerGeneratedFiles to see the output.
Roslyn parses C# into syntax trees, binds names and types into a semantic model, then lowers high-level constructs into simpler ones before emitting IL. foreach over an array becomes an index loop; over `List<T>` it uses the struct enumerator in a try/finally; using becomes try/finally with Dispose; lock becomes Monitor.Enter(obj, ref lockTaken) / Monitor.Exit (or Lock.EnterScope() for System.Threading.Lock); a + b + c on strings becomes one string.Concat; a switch on strings may become a length or hash-based jump. async methods and iterators become state machines. Roslyn does little optimisation; the JIT does the heavy work.
How to tell it (STAR)
Before [the logging upgrade], we had [about 3,000] call sites to change. Regex failed, so I wrote a Roslyn tool that used the semantic model to find exact overloads and rewrote them. The diff was clean and reviewable, and I added an analyser so the old API could not return.
Last updated 31 Oct 2026.