Auto-generate an API reference tree for a class library
Wire the reflection metadata backend (a compiled .dll + .xml pair), call AddApiReference, and get one /reference/api/{type}/ page per public type plus inline Mdazor components for member tables, summaries, and extension-method catalogs.
To ship a DocSite whose reference section stays in sync with a class library's public API, register a metadata backend and call AddApiReference(). One Razor template renders every public type, and a handful of Mdazor components (<ApiMemberTable>, <ApiSummary>, <ExtensionMethods>, <ApiParameterTable>) are available inline in markdown for hand-authored reference pages. Every generated page, search entry, and xref keys off a single pass over the configured backend.
Pennington.ApiMetadata.Reflection.AddApiMetadataFromCompiledAssembly() is the metadata backend: it reflects over a compiled .dll and parses the companion xmldoc .xml file. It documents any assembly — one you build alongside the docs host, or a third-party NuGet package — without needing its source.
Before you begin
AddApiReferenceruns afterAddDocSite. It appends its own assembly toDocSiteOptions.AdditionalRoutingAssembliesat registration time, soAddDocSitemust already be wired.- One metadata backend is registered before
AddApiReference. Without one, the content service has nothing to publish.
Wire the reflection backend
Add a project reference to Pennington.ApiMetadata.Reflection, add a <PackageReference> to the library you want to document, and have Pennington resolve the assembly by simple name. A complete single-package DocSite host:
using Pennington.ApiMetadata.Reflection;
using Pennington.DocSite;
using Pennington.DocSite.Api;
var builder = WebApplication.CreateBuilder(args);
// Standard DocSite wiring. No Areas — single default TOC.
builder.Services.AddDocSite(() => new DocSiteOptions
{
SiteTitle = "FusionCache Docs",
SiteDescription = "Demo of Pennington's reflection-based API metadata backend, documenting ZiggyCreatures.FusionCache straight from its NuGet package.",
GitHubUrl = "https://github.com/ZiggyCreatures/FusionCache",
HeaderContent = """<a href="/" class="font-bold text-lg">FusionCache Docs</a>""",
FooterContent = """<footer class="mt-16 py-8 text-center text-sm text-base-500">FusionCache © ZiggyCreatures. Rendered by Pennington.</footer>""",
});
// Reflection-backed API metadata sourced from the ZiggyCreatures.FusionCache
// NuGet package reference in the .csproj — no live compilation, no staged
// dll/xml, no vendored source.
builder.Services.AddApiMetadataFromCompiledAssembly(opts =>
opts.FromPackageReference("ZiggyCreatures.FusionCache"));
// Auto-publishes /api/{slug}/ pages off the metadata provider and registers
// the <ApiSummary>, <ApiMemberTable>, <ApiParameterTable>, ... Mdazor
// components. Sits in the Guides section of the sidebar so readers drop into
// the full type index from the same nav they read the stampede/fail-safe
// guides in.
builder.Services.AddApiReference(configure: opts =>
{
opts.RoutePrefix = "/api/";
opts.TocSectionLabel = "Guides";
});
var app = builder.Build();
app.UseDocSite();
await app.RunDocSiteAsync(args);
FromPackageReference resolves the .dll (and its companion xmldoc .xml) from a matching <PackageReference> by simple name — no staged dll, no committed binary, and bumping the documented version is a <PackageReference Version=…> change.
When the target isn't a normal NuGet reference — a locally-built assembly, a single-file bundle, or something else without a file location — fall back to the explicit form:
builder.Services.AddApiMetadataFromCompiledAssembly(opts =>
opts.AssemblyFiles.Add(Path.Combine(builder.Environment.ContentRootPath, "lib", "net9.0", "Foo.dll")));
The reflection backend loads each .dll into a MetadataLoadContext — it inspects metadata without running the assembly's code, so it needs no MSBuild workspace and no source. It resolves <inheritdoc/> and union-case xmldoc from that metadata. Because it reads metadata rather than source text, this backend can populate the <ApiSummary> and <ApiMemberTable> components below, but it cannot back a fence that embeds a member's source — the kind of resolver you would write with an ICodeBlockPreprocessor has nothing to read.
Customize the route prefix
The default prefix is /reference/api/. Override it per registration via AddApiReference's RoutePrefix option when the shorter /api/ (or any other prefix) is a better fit:
builder.Services.AddApiMetadataFromCompiledAssembly(opts => { /* ... */ });
builder.Services.AddApiReference(configure: opts => opts.RoutePrefix = "/api/");
Type pages land at /api/{slug}/, and xref uids stay as reference.api.{slug} for back-compat with the default registration.
Document multiple libraries on one site
Pair each library with its own named AddApiMetadataFrom* call and a matching AddApiReference call. Names are the key that wires a reference tree to its provider, and every tree gets its own URL prefix:
builder.Services.AddApiMetadataFromCompiledAssembly("spectre-console", opts =>
opts.FromPackageReference("Spectre.Console"));
builder.Services.AddApiMetadataFromCompiledAssembly("spectre-console-cli", opts =>
opts.FromPackageReference("Spectre.Console.Cli"));
builder.Services.AddApiReference("spectre-console", opts =>
opts.RoutePrefix = "/api/spectre/");
builder.Services.AddApiReference("spectre-console-cli", opts =>
opts.RoutePrefix = "/api/spectre-cli/");
Each FromPackageReference call resolves one DLL from its matching <PackageReference>. Cross-package type references resolve automatically across the NuGet cache root.
Cross-references between named trees: uids pick up a qualifier. Default-named registrations emit reference.api.{slug} (unchanged). Named registrations emit reference.api.{name}.{slug} — for example, <xref:reference.api.spectre-console.ansi-console> and <xref:reference.api.spectre-console-cli.command-app>.
Hand-authored markdown: components like <ApiSummary> auto-pick up the source from the enclosing generated page. For markdown pages outside the generated tree that reach into a specific named registration, add an explicit Source attribute:
<ApiSummary XmlDocId="T:Spectre.Console.Cli.CommandApp" Source="spectre-console-cli" />
Narrow what gets published
The reflection backend documents the assemblies you point it at, so narrowing is a matter of which assemblies you add. The built-in rules already exclude types that are not public, are delegates or attributes, derive from ComponentBase, or carry no xmldoc <summary>.
Use AssemblyFiles to document an explicit list of .dll paths when a folder holds more assemblies than you want documented — for example, dependencies copied alongside the target only so MetadataLoadContext can resolve them:
builder.Services.AddApiMetadataFromCompiledAssembly(opts =>
{
opts.AssemblyFiles.Add(Path.Combine(libDir, "MyLibrary.dll"));
opts.AssemblyFiles.Add(Path.Combine(libDir, "MyLibrary.Extensions.dll"));
});
Use AssemblyDirectories instead to document every .dll/.xml pair in a folder — the typical NuGet lib/<tfm>/ layout.
Render reference fragments inline
Summarize one symbol with <ApiSummary>
Pulls the <summary> tag off a type or member and renders it as prose. Pass an xmldocid as XmlDocId.
<ApiSummary XmlDocId="T:Pennington.ApiMetadata.ApiTypeSummary" />
Lightweight header describing a documented type, used for listings, slug disambiguation, and cross-link display names.
Enumerate type members with <ApiMemberTable>
Kind="All" groups members by category (Properties, Constructors, Fields, Methods, Events) with headings between; narrow it with Kind="Properties" or Kind="Methods" for a single bucket.
<ApiMemberTable XmlDocId="T:Pennington.ApiMetadata.ApiTypeSummary" Kind="Properties" />
Assemblystring- Declaring assembly name without extension.
FullTypeNamestring- Fully-qualified type name (namespace + dot + type name).
KindApiTypeKind- Category of the type.
Namestring- Short type name without namespace (e.g.
ContentPipeline). Namespacestring- Fully-qualified containing namespace, empty for the global namespace.
Summarystring- First-sentence plain-text summary, or
nullwhen no xmldoc summary is available. Uidstring- Canonical xmldocid (e.g.
T:Namespace.TypeName). Normalized to xmldocid form regardless of source backend.
List a method's parameters with <ApiParameterTable>
Pass a method xmldocid (M:...). The table pulls parameter names and types from the provider's pre-formatted ApiMember and descriptions from each <param> tag.
<ApiParameterTable XmlDocId="M:Pennington.ApiMetadata.IApiMetadataProvider.GetMembersAsync(System.String,Pennington.ApiMetadata.MemberKind,Pennington.ApiMetadata.AccessFilter,Pennington.ApiMetadata.MemberOrder)" />
typeUidstring- Uid of the type whose members are returned.
kindMemberKind- Member categories to include (properties, methods, and so on).
accessAccessFilter- Accessibility levels to include.
orderMemberOrder- Sort order applied to the returned members.
Catalog extension methods by receiver with <ExtensionMethods>
Groups every public extension method in the assembly by the unqualified short name of its first (receiver) parameter. Receiver="IServiceCollection" gathers every services.AddX() helper the library ships.
<ExtensionMethods Receiver="IServiceCollection" />
Result
Every public type with an xmldoc comment gets a route under /reference/api/{slug}/:
/reference/api/ -> uid: reference.api
/reference/api/api-type-summary/ -> uid: reference.api.api-type-summary
/reference/api/api-member/ -> uid: reference.api.api-member
Xref links like <xref:reference.api.api-type-summary> resolve, the pages flow through search and llms.txt, and the index page at /reference/api/ lists every type grouped by namespace. One TOC entry — the index page, titled "API reference" by default — appears in the sidebar; per-type pages stay out of the sidebar and are reached via type-name search, xref links, and the index. Override TocTitle and TocSectionLabel on ApiReferenceRegistrationOptions to customize, or set TocTitle = null to suppress the index entry entirely.
Verify
- Run
dotnet runand visit/reference/api/— expect one<li>per public documented type, grouped by namespace. - Visit
/reference/api/{some-type-slug}/for a type you know has an xmldoc<summary>— expect the summary prose and a member table grouped by kind. - Add
<xref:reference.api.{slug}>to any markdown page and confirm it resolves to the generated page after a rebuild.
Related
- How-to: Source content from outside the markdown pipeline — hand-write an
IContentServicewhenAddApiReference's discovery rules are not the right fit. - Reference: Pennington.ApiMetadata.ApiTypeSummary, Pennington.ApiMetadata.ApiMember.