Thunder.DocSnippets

Make your C# XML doc <code> examples runnable as unit tests — the Rust doctest experience for .NET.

DocSnippets is a Roslyn source generator. Add the package to your test project, and every <code> block in your library's XML documentation becomes a compiled, executable NUnit/xUnit/MSTest test. No extra tooling, no checked-in generated files.


Quick start

  1. Add DocSnippets to your test project:

    <PackageReference Include="Thunder.DocSnippets" Version="..." />
    
  2. Run dotnet test. Done.

DocSnippets ships an MSBuild .targets file that automatically wires up the adjacent source project's .cs files as AdditionalFiles — no manual <ItemGroup> configuration required.


Writing snippets

Any <code> block in an XML doc comment is picked up automatically in opt-out mode (the default):

/// <summary>Adds two integers.</summary>
/// <example>
/// <code>
/// var result = calculator.Add(1, 2); // => 3
/// </code>
/// </example>
public int Add(int a, int b) => a + b;

The assertion comment is transformed into a framework-appropriate equality check at generation time. No assertion library is needed in the snippet itself.

Inline assertion patterns

All four comment styles work on both declaration lines and expression lines:

Pattern Example Meaning
// => VALUE var x = f(); // => 42 Assert x equals 42
// => ~VALUE var x = f(); // => ~3.14 Assert x3.14 (±0.05)
// => ~VALUE (Ndp) var x = f(); // => ~3.14 (2dp) Assert x3.14 within 2 decimal places
// result: VALUE var x = f(); // result: 42 Assert x equals 42
// VARNAME: VALUE DoSetup(); // myVar: 42 Assert myVar equals 42
// VALUE (bare literal) var x = f(); // 42 Assert x equals 42

On a declaration line (var x = f(); // => 3), the generator splits the line into the declaration statement and a separate assertion using the declared variable name. On an expression line (f(); // => 3), the entire expression becomes the assertion subject.

Bare literal comments (// 42, // true, // "hello") are only treated as assertions when the comment contains an unambiguous C# literal. Non-literal comments (// sum of values, // see above) are left unchanged.

Excluding a snippet

Add // doctest-ignore on the first line of any <code> block to skip it:

/// <code>
/// // doctest-ignore
/// // This example is illustrative — not executable.
/// var x = SomeExternalCall();
/// </code>

Compile-only snippets

Some examples are valid documentation but not self-contained — they reference live objects (GraphicsDevice, SpriteBatch, etc.) that can't be instantiated in a test without boilerplate that would pollute the rendered docs. Mark these with // doctest-compile-only:

/// <code>
/// // doctest-compile-only
/// spriteBatch.Begin();
/// spriteBatch.Draw(texture, position, Color.White);
/// spriteBatch.End();
/// </code>

The generated test is compiled (so rename refactors and API breakages are caught) but skipped at runtime. DocSnippets automatically stubs any undeclared identifiers (spriteBatch, texture, position) by looking them up in the project's symbol table — no manual stub declarations needed. The skip attribute varies by framework:

Framework Attribute
NUnit [Test, Ignore("compile-only")]
xUnit [Fact(Skip = "compile-only")]
MSTest [TestMethod, Ignore]

Assertion comments (// => VALUE) are not transformed in compile-only snippets — they are emitted verbatim as comments.

Opt-in mode

By default all blocks run. To run only explicitly marked blocks, set snippetMode: "opt-in" in docsnippets.json, then mark each runnable snippet with // doctest:

/// <code>
/// // doctest
/// var result = calculator.Add(1, 2); // => 3
/// </code>

Configuration (docsnippets.json)

Drop a docsnippets.json file alongside the source files (the shipped MSBuild targets pick it up automatically):

{
  "snippetMode": "opt-out",
  "assertionStyle": "NUnit",
  "implicitUsings": ["MyLib", "MyLib.Models"]
}
Key Values Default Description
snippetMode "opt-out", "opt-in" "opt-out" opt-out: all <code> blocks run. opt-in: only blocks marked // doctest run.
assertionStyle "NUnit", "XUnit", "MSTest" (auto-detect) Override the auto-detected test framework.
implicitUsings Array of namespace strings [] Namespaces added as using to every generated test class.

InternalsVisibleTo

If your snippets reference internal types, add this to your source project:

[assembly: InternalsVisibleTo("YourTestProject")]

Without it, snippets referencing internal members produce CS0122 errors.


Known limitations

  • Tuple deconstructionvar (a, b) = f(); // => ... is not supported on the declaration line. Put the assertion on a separate line: a // => 1.
  • Async snippets — snippets containing await produce a compile error. The emitted test method is synchronous. Async support is planned for a future release.
  • Single-assembly scanning — only direct ProjectReference source projects are scanned. Transitive references are not included.
latest ▼