SourceDocParser.Common
2.0.0
dotnet add package SourceDocParser.Common --version 2.0.0
NuGet\Install-Package SourceDocParser.Common -Version 2.0.0
<PackageReference Include="SourceDocParser.Common" Version="2.0.0" />
<PackageVersion Include="SourceDocParser.Common" Version="2.0.0" />
<PackageReference Include="SourceDocParser.Common" />
paket add SourceDocParser.Common --version 2.0.0
#r "nuget: SourceDocParser.Common, 2.0.0"
#:package SourceDocParser.Common@2.0.0
#addin nuget:?package=SourceDocParser.Common&version=2.0.0
#tool nuget:?package=SourceDocParser.Common&version=2.0.0
<br> <a href="https://github.com/glennawatson/SourceDocParserLib"> <img width="160" height="160" src="https://raw.githubusercontent.com/glennawatson/SourceDocParserLib/main/icons/SourceDocParserIcon.png" alt="SourceDocParserLib"> </a> <br>
SourceDocParserLib
Roslyn-based .NET assembly walker that turns compiled .dll + .pdb + .xml
triples into a strongly-typed API catalog (types, members, signatures, XML
docs, <inheritdoc/>, SourceLink) and hands it to a pluggable
IDocumentationEmitter for rendering.
The catalog is format-neutral. Emitters decide how to render it — Markdown
for Zensical / mkdocs Material, or YAML for docfx ManagedReference, with room
for other targets. Pages flow through an IPageSink so callers can write to
disk (FilePageSink) or pipe straight into another async pipeline
(CallbackPageSink) without staging files on disk.
Logging flows through Microsoft.Extensions.Logging.Abstractions
source-generated [LoggerMessage] partials, so any host (Serilog, Console,
NLog, …) plugs in without the libraries taking a dependency on a specific
backend.
Packages
Each package is shipped to NuGet independently; the badge tracks the current published version.
Core
| Package | NuGet | What |
|---|---|---|
SourceDocParser |
Walker, merger, source-link resolution. Defines IAssemblySource, IDocumentationEmitter, IMetadataExtractor, the IPageSink streaming contract (FilePageSink + CallbackPageSink), the ICrefResolver cross-link seam, and the shared CatalogIndexes rollup (derived classes / extension methods / inherited members). |
|
SourceDocParser.Common |
Shared primitives consumed by every other package: pooled StringBuilder rentals, span-based path / token helpers, allocation-free identifier formatting. No public API stability guarantee yet — pinned via the same MinVer baseline as the rest. |
Assembly sources
| Package | NuGet | What |
|---|---|---|
SourceDocParser.NuGet |
IAssemblySource that fetches packages from nuget.org by owner / explicit list (nuget-packages.json) and exposes the per-TFM lib/ + refs/ trees. Shares one tuned HttpClient across the fetch; disposes alongside the source. |
Emitters
| Package | NuGet | Builder | What |
|---|---|---|---|
SourceDocParser.Zensical |
new ZensicalDocumentationEmitter() |
Writes Markdown tuned for Zensical / mkdocs Material — admonitions, content tabs, mermaid, MD-style cross-links via the autoref UID convention. | |
SourceDocParser.Docfx |
new DocfxYamlEmitter() |
Writes docfx ManagedReference YAML pages (drop-in replacement for dotnet docfx metadata output) plus the docfx.json config-file shim that lets an existing docfx site drive the parser pipeline. |
Quick start
using Microsoft.Extensions.Logging;
using SourceDocParser;
using SourceDocParser.NuGet.Infrastructure;
using SourceDocParser.Zensical;
var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
using var source = new NuGetAssemblySource(
rootDirectory: "/path/to/repo", // contains nuget-packages.json
apiPath: "/path/to/api", // where lib/ + refs/ get extracted
logger: loggerFactory.CreateLogger<NuGetAssemblySource>());
var emitter = new ZensicalDocumentationEmitter();
// File-rooted sink for the legacy on-disk shape …
var sink = new FilePageSink("/path/to/markdown-output");
var result = await new MetadataExtractor().RunAsync(
source,
sink,
emitter,
loggerFactory.CreateLogger<MetadataExtractor>());
Console.WriteLine($"Emitted {result.PagesEmitted} pages across {result.CanonicalTypes} types.");
Streaming pages instead of writing to disk
CallbackPageSink invokes a (string relativePath, byte[] utf8Bytes)
callback per page — useful when piping the output into another pipeline
(e.g. an in-memory render queue) without staging anything on disk:
var sink = new CallbackPageSink((relativePath, bytes) =>
{
Console.WriteLine($"{relativePath}: {bytes.Length} bytes");
});
await new MetadataExtractor().RunAsync(source, sink, emitter, logger);
The bytes are encoded once into a freshly-allocated byte[] so the callback
owns them outright — safe to retain, no array-pool ties.
Supported target frameworks
The walker resolves NuGet packages against frameworks that the active .NET SDK still understands and that Microsoft is still shipping fixes for:
- Modern .NET (5.0+) —
net5.0,net6.0,net7.0,net8.0,net9.0,net10.0, plus thenet*-android,net*-ios,net*-maccatalyst,net*-windowsworkload variants. See the official .NET and .NET Core support policy. - netstandard —
netstandard1.0throughnetstandard2.1. Sticks around because the BCL targets it, even though no future netstandard releases are planned. - .NET Framework, net462 and newer —
net462,net47,net471,net472,net48,net481. net462 is the floor that supportsnetstandard2.0type forwards and ships ref packs in modern SDKs. See the .NET Framework support policy for which of those are still in mainstream / extended support.
Out of scope (legacy, not supported):
| Family | Examples | Why |
|---|---|---|
| Xamarin | xamarinios*, xamarinmac*, xamarintvos*, xamarinwatchos* |
Support ended 1 May 2024; the workloads moved to .NET MAUI under the modern net*-android / net*-ios / net*-maccatalyst / net*-tvos TFMs. |
| Legacy Mono profiles | MonoAndroid*, MonoTouch* |
Predecessors of the Xamarin workloads. Same end-of-support story. |
| .NET Framework < 4.6.2 | net20, net35, net40, net45, net451, net46, net461 |
Out of mainstream support, and pre-net462 doesn't carry netstandard 2.0 type forwards so the resolver can't reuse modern surface against them. |
| Silverlight | sl* |
Microsoft retired Silverlight on 12 October 2021. |
| Windows Phone | wp*, wpa* |
Windows Phone 8.1 end-of-support was 11 July 2017; the platform itself was discontinued. |
| Windows Store / UAP | win8, winrt*, uap* |
UWP apps are now expected to migrate to Windows App SDK / WinUI 3. |
| Portable Class Libraries | portable-* profiles |
Replaced by netstandard a decade ago. |
Packages that ship only legacy TFMs are skipped at fetch time. Skips are
logged at information level so they don't drown out genuine warnings on
real-world walks (ReactiveUI / Avalonia / Splat surfaces typically pull
several dozen System.* packages whose entire TFM list is legacy-only). Set
your logger filter to Information if you want to see the legacy-skip list
during a build.
Performance
Benchmark workload. Numbers below are from the BenchmarkDotNet suite
under src/benchmarks/SourceDocParser.Benchmarks/, run on a Ryzen 7 5800X /
.NET 10. The workload extracts three NuGet packages from nuget.org
— pulling each package's lib/ and ref/ trees and the matching reference
assemblies, walking every public symbol across ~19 target-framework groups,
parsing the shipped XML doc files, resolving <inheritdoc/> chains, and
emitting roughly 600 canonical type pages after cross-TFM merge. The local
NuGet cache is warmed once during global setup so per-iteration timings
measure the walk + merge + emit pipeline, not the network leg.
End-to-end (MetadataExtractor.RunAsync):
| Phase | Wall time | Allocated |
|---|---|---|
Full pipeline (RunAsync) |
~1.5 s | ~525 MB |
| Discover (NuGet config + cache scan) | ~990 ms | ~258 MB |
| Load + walk (parallel, all groups) | ~509 ms | ~236 MB |
| Merge (cross-TFM dedup) | ~1 ms | ~380 KB |
| Emit (Zensical Markdown) | ~139 ms | ~39 MB |
The walk phase walks one Roslyn compilation per package — one canonical TFM
per equivalence class. Other TFMs whose public-API surface is a subset of
the canonical's are folded in via a MetadataReader probe that only
enumerates type tokens, no symbol tree, no constructed types. The merger
then broadcasts the canonical's walked types into each subset TFM so
ApiType.AppliesTo still records every TFM the type applies to.
Per-call hotspots:
| Operation | Time | Allocated |
|---|---|---|
XmlDocToMarkdown.Convert — plain summary |
~24 ns | 176 B |
XmlDocToMarkdown.Convert — tagged with <see> / <c> / <paramref> |
~916 ns | 456 B |
XmlDocToMarkdown.Convert — code block + bullet list |
~1.2 µs | 440 B |
TfmResolver.FindBestRefsTfm — exact match |
~3 ns | 0 B |
TfmResolver.FindBestRefsTfm — platform-suffix strip |
~11 ns | 0 B |
TfmResolver.FindBestRefsTfm — netstandard fallback |
~496 ns | 1 KB |
TypeMerger.Merge — 600 types × 3 TFMs |
~115 µs | 358 KB |
Emitter cost per type page (no I/O, just markup formatting; baseline = Zensical Markdown):
| Workload (types × members/type) | Zensical Markdown | DocFx YAML | Time | Alloc |
|---|---|---|---|---|
| 100 × 5 | 72 µs / 288 KB | 618 µs / 1,366 KB | 8.6× | 4.7× |
| 100 × 30 | 263 µs / 763 KB | 5,432 µs / 6,338 KB | 20.7× | 8.3× |
| 600 × 5 | 437 µs / 1,730 KB | 3,605 µs / 8,198 KB | 8.3× | 4.7× |
| 600 × 30 | 1,505 µs / 4,580 KB | 17,122 µs / 38,025 KB | 11.4× | 8.3× |
DocFx YAML is heavier by design — every member duplicates uid / commentId /
parent / name / nameWithType / fullName, and the page-level references:
list adds another mapping per cross-referenced type. The emitter hand-writes
YAML through StringBuilder (no YamlDotNet runtime dependency), with a
single-allocation fast path for qualified-name composites that round-trip
identifiers as plain scalars when escape-safe.
How perf and allocations stay low
- MetadataReader probe + canonical-only Roslyn walk. The walker only spins up one Roslyn compilation per package — the canonical TFM picked by descending rank. Other TFMs whose public type set is a subset of the canonical's are detected via a
System.Reflection.Metadata.MetadataReaderprobe (no symbol binding, no constructed-type allocation) and folded intoApiType.AppliesTovia a synthetic broadcast catalog that reuses the canonical's already-walked types. TFMs whose surface is not a subset still get a full Roslyn walk so removed-in-newer-TFM types stay in the catalog. - Custom span-based XML scanner. A
ref struct DocXmlScannerwalks///doc fragments directly overReadOnlySpan<char>, implementing just the XML grammar doc comments use.XmlReader'sXmlTextReaderImplallocates multi-KB internal buffers (NodeData[],NamespaceManager, char buffers) per construction; the scanner avoids that. Both the per-symbol parser and the Markdown renderer drive it, so per-element XML processing is allocation-free apart from the result string. - Build-once-then-read-many
XmlDocSource. Each.xmldoc file is read once viaFile.ReadAllBytes+Encoding.UTF8.GetStringand indexed by per-member(offset, length)ranges; substrings materialise only when a consumer callsGet(memberId). Safe for concurrent reads from the parallel walker. - Eager per-group loader disposal. Each TFM group's
CompilationLoaderholds memory-mapped views of every reference DLL. An interlocked counter retires the loader as soon as its last assembly finishes; peak working set scales with the slowest-finishing group, not the total number of groups times their references. - Streaming type merger. The parallel walk feeds
ApiCatalogs intoStreamingTypeMergerone at a time and immediately drops the reference. Catalogs don't accumulate in aConcurrentBagwaiting for the walk phase to finish. - Streaming page sink.
IPageSinklets the emitter hand each page off as bytes the moment it's rendered —FilePageSinkflushes throughPageWriter(chunked UTF-8 viaArrayPool<byte>into an unbufferedFileStream);CallbackPageSinkinvokes a delegate so callers can pipe into aChannel, an HTTP body, or another in-process pipeline without ever staging files on disk. - Capture-free parallel dispatch. The
Parallel.ForEachAsynclambda isstatic; every dependency it touches is bundled into aWalkContextrecord attached to each work item, so dispatch never allocates a closure object per assembly. - Lazy
RenderedDocfacade for emit-time conversion. Walker output carries raw inner-XML fragments. Each emitter constructs anXmlDocToMarkdown(ICrefResolver)and wraps each symbol's documentation in aRenderedDocthat converts each text-shaped field on first read, caches the result, and skips fields the page doesn't consume. Zensical and docfx pick their own cref form ([name][uid]autoref vs<xref:uid>/ Microsoft Learn URL) without the walker baking either in. - Thread-static
PageBuilderPool. Each emit thread reuses oneStringBuilderacross page composition calls via ausing-scoped rental; pages clear the builder between uses instead of allocating fresh. - Shared
CatalogIndexesrollup. Derived-class lookup, reverse extension-method lookup, and per-type inherited-member uid lists are built once per emit run in a single O(N) sweep and frozen viaFrozenDictionary. Each emitter passes its ownSystem.Objectbaseline UIDs (docfx bare names, ZensicalM:-prefixed commentIds) so the algorithm stays shared while the wire format stays per-emitter. - Pre-sized buffers and stackalloc paths. nupkg zip entries size their backing
byte[]to the known uncompressed length up front. SourceLink URL rewriting andZensicalCrefResolver's Microsoft Learn link composer build their result strings viastackalloc+new string(span)so the only heap allocation is the returned string itself.
Repository layout
SourceDocParserLib/
icons/SourceDocParserIcon.png package icon (packed into every nupkg)
src/
SourceDocParser/ core walker / merger / sinks
SourceDocParser.Common/ shared primitives
SourceDocParser.NuGet/ nuget.org IAssemblySource
SourceDocParser.Docfx/ docfx YAML emitter
SourceDocParser.Zensical/ mkdocs-Material Markdown emitter
benchmarks/ BenchmarkDotNet harness
tests/
SourceDocParser.Tests/ unit tests (TUnit)
SourceDocParser.NuGet.Tests/
SourceDocParser.Zensical.Tests/
SourceDocParser.Docfx.Tests/
SourceDocParser.IntegrationTests/ end-to-end + Zensical render-smoke
Directory.Build.props shared lib config (MinVer, packing, analyzers)
Directory.Packages.props central package versions
SourceDocParserLib.slnx
.editorconfig
stylecop.json
dotnet build from src/ packs every non-test project into
artifacts/packages/ automatically (<GeneratePackageOnBuild>true</GeneratePackageOnBuild>).
Consumers in other repos can wire that directory up as a local feed via
nuget.config until the libraries are published.
Versioning
MinVer derives the version from the most recent v* git tag. Untagged
commits build as {nextMinor}.0.0-alpha.0.{height}+sha (auto-increment
minor). Releases run via the Release workflow (workflow_dispatch) — pick
major / minor / patch; the workflow computes the next version from the
latest RTM tag in shell, creates the v$VERSION tag, propagates the version
via MINVERVERSIONOVERRIDE so every downstream MSBuild project skips
MinVer's per-project git walk, then builds / packs / signs / pushes to
NuGet and creates the GitHub Release.
Acknowledgements
The metadata extraction pipeline is inspired by — and lifts patterns from
— dotnet/docfx (MIT licensed). docfx's
Roslyn-based assembly walker, inheritdoc resolution, and overall metadata
model shaped this library's design. See LICENSE for the
original docfx attribution.
Built on:
- Roslyn (Microsoft.CodeAnalysis.CSharp) for compilation + symbol model
- ICSharpCode.Decompiler for transitive reference resolution
- NuGet.Frameworks + NuGet.Versioning for proper TFM compatibility and SemVer ordering
- Polly v8 for HTTP retry/rate-limit pipelines
License
MIT — see LICENSE for the full text and the docfx attribution.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on SourceDocParser.Common:
| Package | Downloads |
|---|---|
|
SourceDocParser.Zensical
Zensical / mkdocs Material emitter for SourceDocParser. Renders the parser's ApiCatalog into a flat tree of Markdown pages tuned for the Zensical theme (admonitions, content tabs, mermaid diagrams). |
|
|
SourceDocParser.Docfx
docfx compatibility for SourceDocParser. Reads and writes docfx.json shapes (metadata + build sections) so an existing docfx site can plug into the parser pipeline, and emits docfx ManagedReference YAML pages so the parser output is consumable by docfx as a drop-in replacement for its own metadata extractor. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0 | 534 | 5/10/2026 |
| 1.4.2 | 774 | 5/2/2026 |
| 1.4.1 | 479 | 4/30/2026 |
| 1.3.1 | 225 | 4/28/2026 |
| 1.2.1 | 120 | 4/28/2026 |
| 1.1.1 | 121 | 4/28/2026 |
| 1.0.5 | 110 | 4/28/2026 |
| 1.0.3 | 102 | 4/28/2026 |
| 0.6.1-alpha | 104 | 4/28/2026 |
| 0.5.1-alpha | 115 | 4/28/2026 |
| 0.4.1-alpha | 102 | 4/28/2026 |
| 0.3.1-alpha | 108 | 4/27/2026 |
| 0.2.1-alpha | 109 | 4/27/2026 |