Code2Obsidian — Roslyn-Powered Call Graph Notes Generator
A .NET CLI that loads a C# solution with Roslyn, extracts user-defined methods, builds a call graph, and emits Obsidian-friendly Markdown notes per file or per method.
Executive Summary
Code2Obsidian is a CLI tool that turns a C# solution into a set of Obsidian notes. It enumerates user-defined methods via Roslyn, builds a call graph, and writes Markdown with wiki-links so you can navigate a codebase like a knowledge graph.
Why I built it
For unfamiliar or fast-moving systems, the hard part isn’t “reading code” — it’s building a mental index:
- Where does this method get called?
- What does it call next?
- What file is the real behavior in?
This tool creates a usable “documentation skeleton” automatically.
Why Roslyn over regex/parsing
A regex-based approach would miss:
- Extension methods (look like instance calls, are actually static)
- Generic instantiations (need
OriginalDefinitionto deduplicate) - Overload resolution (which
Process()is actually called?) - Assembly boundaries (is this our code or a library?)
Roslyn’s semantic model resolves all of these correctly, giving an accurate call graph instead of a noisy approximation.
How It Works
It loads the solution using MSBuildWorkspace, walks all C# documents, extracts method symbols, and records caller→callee relationships.
Solution load and MSBuild registration
The tool registers MSBuild robustly using Microsoft.Build.Locator, preferring Visual Studio instances when available and falling back to the .NET SDK.
// Two-tier MSBuild registration: VS instance first, SDK fallback
var vs = MSBuildLocator.QueryVisualStudioInstances()
.OrderByDescending(i => i.Version)
.FirstOrDefault();
if (vs is not null) { MSBuildLocator.RegisterInstance(vs); return; }
// Fallback to .NET SDK when no VS installed
var candidate = Directory.GetDirectories(sdkDir)
.OrderByDescending(Path.GetFileName)
.FirstOrDefault(d => File.Exists(Path.Combine(d, "Microsoft.Build.dll")));
MSBuildLocator.RegisterMSBuildPath(candidate);This fallback chain handles development machines (VS), CI servers (SDK only), and container environments where DOTNET_ROOT may be set explicitly.
Extraction strategy
For each document:
- Parse syntax tree
- Use semantic model to get declared symbols for methods
- Filter to “user methods” (exclude external assemblies)
- For each invocation, resolve the target method symbol
- Normalize extension/reduced methods to a canonical symbol form
// Normalize extension methods and generics to canonical form
var target = (info.Symbol as IMethodSymbol)
?? (info.CandidateSymbols.FirstOrDefault() as IMethodSymbol);
var canon = target.ReducedFrom // Extension method → static form
?? target.OriginalDefinition // Generic → unbound definition
?? target;Extension methods appear differently depending on call syntax (str.IsNullOrEmpty() vs string.IsNullOrEmpty(str)). The ReducedFrom property normalizes both to the same underlying symbol, ensuring accurate edge counting in the call graph.
Graph strategy
callsOut[caller] = set of calleescallsIn[callee] = set of callers(reverse edges)
// Bidirectional call graph with SymbolEqualityComparer
var callsOut = new Dictionary<IMethodSymbol, HashSet<IMethodSymbol>>(
SymbolEqualityComparer.Default);
// Build forward edges during AST walk...
// Then compute reverse edges in O(E)
var callsIn = new Dictionary<IMethodSymbol, HashSet<IMethodSymbol>>(
SymbolEqualityComparer.Default);
foreach (var (caller, callees) in callsOut)
foreach (var callee in callees)
callsIn.GetOrCreate(callee).Add(caller);Using SymbolEqualityComparer.Default is critical—Roslyn symbols are reference-unequal even when logically identical. The reverse edge computation enables Obsidian’s “backlinks” feature: click into any method and immediately see what calls it.
This gives Obsidian-style “backlinks” for free.
Output Modes
Two modes: generate one markdown file per source file, or one markdown file per method.
Per-file mode
- One
.mdper source file - Each method becomes a section with:
- docstring (if present)
- TODO slots (“what it does”, “improvements”)
- Calls → and Called-by ← lists using wiki-links
Per-method mode
- One
.mdper method - Great for building a “method graph” vault that mirrors the call graph
Why YAML frontmatter exists
Each note includes tags like file or method, making it easy to build saved searches, dashboards, and filtered views in Obsidian.
Technologies Used
- .NET 8
- Roslyn Workspaces + MSBuildWorkspace
- Microsoft.Build.Locator
- Plain CLI (no heavy dependencies)