Embed focused code samples
To limit a code fence to the one member a walkthrough discusses — rather than dumping the whole enclosing type with its xmldoc and every sibling property — use the xmldocid preprocessor's member-scoped forms. This page works through four techniques in order of reach: fence one member with M:, strip declaration noise with ,bodyonly, refactor long methods into named helpers, and compare versions with xmldocid-diff. For the fence grammar itself, see Code-block argument reference.
Assumptions
- An existing Pennington site (see Create your first Pennington site if not), with
Pennington.Roslynwired throughAddPenningtonRoslynandSolutionPathpointing at the solution that owns the source to fence. - Comfort authoring markdown code fences — the techniques on this page are all info-string changes on a
csharp:xmldocidfence.
For a working setup, see examples/FocusedCodeSamplesExample. MonolithWordCounter carries one long CountWords method; ModularWordCounter splits the same logic into Tokenize, Tally, and Format. Both are referenced by the fences below.
Fence one member, not the whole type
When the surrounding prose is about one method, reach for M:Type.Method(...) instead of T:Type. Member-scoped forms (M: for methods, P: for properties, F: for fields, E: for events) shrink the fence to the member the reader cares about. A T: fence pulls the full class declaration, its xmldoc, and every sibling member.
The wide form, which lands on a page that only discusses CountWords:
```csharp:xmldocid
T:FocusedCodeSamplesExample.MonolithWordCounter
```
The narrow form, scoped to the method under discussion:
```csharp:xmldocid
M:FocusedCodeSamplesExample.MonolithWordCounter.CountWords(System.String,System.Int32)
```
Which renders as:
/// <summary>
/// Returns a column-aligned report of the <paramref name="topN"/> most
/// frequent words in <paramref name="text"/>, lower-cased and with
/// surrounding punctuation stripped.
/// </summary>
/// <param name="text">Free-form text to analyse.</param>
/// <param name="topN">Number of top-frequency words to include.</param>
/// <returns>A multi-line string suitable for console output.</returns>
public static string CountWords(string text, int topN)
{
// Tokenize: split on whitespace, lowercase, strip surrounding punctuation.
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
// Tally: count occurrences and rank by frequency desc, then alphabetically.
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
var ranked = counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
// Format: header line plus one word-count row per entry.
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
}
The xmldocid grammar for each member kind — T:, M:, P:, F:, E: — is listed in Code-block argument reference.
Strip declaration noise with ,bodyonly
Even a member-scoped M: fence still carries the leading /// <summary> xmldoc and the method signature. When the prose has already named the method and summarized what it does, both are redundant. Appending ,bodyonly renders only the body between the braces.
```csharp:xmldocid,bodyonly
M:FocusedCodeSamplesExample.MonolithWordCounter.CountWords(System.String,System.Int32)
```
Which renders as:
// Tokenize: split on whitespace, lowercase, strip surrounding punctuation.
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
// Tally: count occurrences and rank by frequency desc, then alphabetically.
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
var ranked = counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
// Format: header line plus one word-count row per entry.
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
,bodyonly also works on types (members between the braces, skipping the class header) and properties (the get/set accessors without the leading xmldoc).
Break a long method into named helpers
When the target method runs 25+ lines across distinct phases, no fence form will make it short and intelligible — the source itself is too large. Fix the source, not the fence: extract each phase into a named helper with its own xmldoc summary, then fence the helpers one at a time.
ModularWordCounter is the same logic as MonolithWordCounter split into three helpers — Tokenize, Tally, and Format — orchestrated by a short CountWords. A T: fence on the whole class gives the reader the full picture in one place:
```csharp:xmldocid
T:FocusedCodeSamplesExample.ModularWordCounter
```
Which renders as:
/// <summary>
/// Word-frequency counter whose parse, tally, and format phases are named
/// public helpers rather than inline blocks. Pair with
/// <see cref="MonolithWordCounter"/> to contrast a decomposed shape against
/// a monolithic one.
/// </summary>
public static class ModularWordCounter
{
/// <summary>
/// Returns a column-aligned report of the <paramref name="topN"/> most
/// frequent words in <paramref name="text"/> by orchestrating the three
/// helpers below.
/// </summary>
/// <param name="text">Free-form text to analyse.</param>
/// <param name="topN">Number of top-frequency words to include.</param>
/// <returns>A multi-line string suitable for console output.</returns>
public static string CountWords(string text, int topN)
{
var words = Tokenize(text);
var ranked = Tally(words, topN);
return Format(ranked);
}
/// <summary>
/// Splits <paramref name="text"/> on whitespace, lower-cases every token,
/// and strips surrounding punctuation. Empty tokens are dropped.
/// </summary>
public static List<string> Tokenize(string text)
{
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
return words;
}
/// <summary>
/// Groups <paramref name="words"/>, counts occurrences, and returns the
/// top <paramref name="topN"/> ranked by frequency descending then
/// alphabetically.
/// </summary>
public static List<KeyValuePair<string, int>> Tally(List<string> words, int topN)
{
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
return counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
}
/// <summary>
/// Renders <paramref name="ranked"/> as a header line plus one
/// column-aligned row per entry.
/// </summary>
public static string Format(List<KeyValuePair<string, int>> ranked)
{
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
}
/// <summary>
/// Same output as <see cref="Format"/>, but rents its
/// <see cref="StringBuilder"/> from <see cref="StringBuilderPool"/>
/// instead of allocating a fresh one each call. Exists to pair with
/// <see cref="Format"/> inside an <c>xmldocid-diff</c> fence so the
/// delta is small and focused on one mechanical change.
/// </summary>
public static string FormatV2(List<KeyValuePair<string, int>> ranked)
{
var sb = StringBuilderPool.Get();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
var result = sb.ToString();
StringBuilderPool.Return(sb);
return result;
}
}
In a walkthrough, fence each helper separately so each section carries one idea:
```csharp:xmldocid,bodyonly
M:FocusedCodeSamplesExample.ModularWordCounter.CountWords(System.String,System.Int32)
```
```csharp:xmldocid,bodyonly
M:FocusedCodeSamplesExample.ModularWordCounter.Tokenize(System.String)
```
```csharp:xmldocid,bodyonly
M:FocusedCodeSamplesExample.ModularWordCounter.Tally(System.Collections.Generic.List{System.String},System.Int32)
```
```csharp:xmldocid,bodyonly
M:FocusedCodeSamplesExample.ModularWordCounter.Format(System.Collections.Generic.List{System.Collections.Generic.KeyValuePair{System.String,System.Int32}})
```
The orchestrator renders as a three-liner that reads top-to-bottom as the outline for the walkthrough:
var words = Tokenize(text);
var ranked = Tally(words, topN);
return Format(ranked);
Tokenize:
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
return words;
Tally:
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
return counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
Format:
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
Keep the helpers public. internal methods do not surface xmldoc and do not participate in the symbol table the preprocessor walks, so fences against them fail to resolve. For doc-facing fixtures, public is the right visibility even when idiomatic application code would keep them internal.
Show a delta with xmldocid-diff
When the article's point is that one version replaces another — a small refactor, a migration, a perf tweak — fence both versions with xmldocid-diff. The preprocessor emits a unified diff so the reader sees the two or three lines that moved rather than comparing two fences by eye. The form works best when the delta is small; whole-method rewrites render every line as changed and bury the point.
ModularWordCounter.FormatV2 is deliberately a one-change variant of Format. It rents its StringBuilder from a pool instead of constructing a fresh one, and returns the builder at the end. Everything else is identical, so the diff collapses to those lines.
```csharp:xmldocid-diff,bodyonly
M:FocusedCodeSamplesExample.ModularWordCounter.Format(System.Collections.Generic.List{System.Collections.Generic.KeyValuePair{System.String,System.Int32}})
M:FocusedCodeSamplesExample.ModularWordCounter.FormatV2(System.Collections.Generic.List{System.Collections.Generic.KeyValuePair{System.String,System.Int32}})
```
Which renders as:
var sb = new StringBuilder();
var sb = StringBuilderPool.Get();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
var result = sb.ToString();
StringBuilderPool.Return(sb);
return result;
The fence body must hold exactly two xmldocids, one per line, in before → after order. ,bodyonly applies to both sides, so the diff compares implementations without xmldoc boilerplate drowning out the change.
Reach for :path only when no xmldocid exists
Four shapes have no C# symbol for the preprocessor to target: top-level-statement Program.cs files, .razor components, markdown or YAML fixtures, and JSON / TOML / config files. For those, <lang>:path embeds the whole file by path relative to the solution directory:
```csharp:path
examples/FocusedCodeSamplesExample/Program.cs
```
For anything with a namespace and a type, prefer :xmldocid. The build fails noisily on an unresolved symbol, so it survives the renames and line shifts that silently break :path fences.
Verify
- Rebuild the site with
dotnet run --project docs/Pennington.Docs -- buildand reload the page — each fence renders at the scope its info string declares, with no carry-over of enclosing-type xmldoc. - Grep
output/**/*.htmlfor<pre>elements taller than 25 lines — those are candidates for a,bodyonlyor member-scoped follow-up pass. - Rename
TokenizetoSplitinexamples/FocusedCodeSamplesExample/ModularWordCounter.csand rebuild — the build report surfaces an unresolvedM:…Tokenize(…)rather than silently rendering nothing.
Related
- Reference: Markdown extensions catalog — the full fence grammar including
xmldocid,xmldocid,bodyonly,xmldocid-diff, andpath. - Reference: Code-block argument reference — info-string parser details and the full list of suffix forms.
- How-to: Annotate code blocks — per-line
[!code highlight]/[!code ++]directives that compose with the fence forms on this page. - Reference: RoslynOptions — the
SolutionPathsetting that lets the preprocessor resolveT:/M:/P:targets.