How C# documentation comments work

C# documentation comments are XML fragments placed immediately before a type or member. They begin with /// for a single-line comment block or /** for a delimited block. When XML documentation output is enabled, the compiler writes the comments to an XML file that documentation tools and IDEs can read.
The official C# documentation comments specification defines the processing rules and recommended tags. Two details matter in day-to-day work:
- the comment must immediately precede the class, interface, method, property, field, delegate, or event it describes;
- the XML must be well formed, and references in tags such as
<param>andcrefcan be checked by the documentation generator.
This is the smallest useful C# documentation example:
/// <summary>
/// Calculates the total after applying a percentage discount.
/// </summary>
/// <param name="subtotal">The amount before the discount.</param>
/// <param name="discountPercent">A value from 0 through 100.</param>
/// <returns>The discounted total.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="discountPercent"/> is outside 0 through 100.
/// </exception>
public static decimal ApplyDiscount(decimal subtotal, decimal discountPercent)
{
if (discountPercent is < 0 or > 100)
{
throw new ArgumentOutOfRangeException(nameof(discountPercent));
}
return subtotal * (1 - discountPercent / 100);
}
The comment answers questions a caller cannot settle from the signature alone: the accepted range, the meaning of the return value, and the exception raised for invalid input. Repeating “applies a discount” in every tag would add volume without helping the reader.
Recommended XML documentation tags
The C# specification recommends a standard set of tags. You do not need all of them on every member.
| Tag | Use it for | Common mistake |
|---|---|---|
<summary> | A concise description shown by IntelliSense | Restating the member name without explaining behavior |
<param> | The meaning, units, limits, or format of a parameter | Writing only “the value” |
<returns> | The returned value and relevant states | Omitting null, empty, or sentinel cases |
<exception> | An exception callers should handle | Listing exceptions the method cannot throw |
<remarks> | Details that do not fit the summary | Hiding critical constraints in a long paragraph |
<example> | A short, representative usage example | Providing code that no longer compiles |
<see> / <seealso> | A checked reference to another code element or resource | Pasting an ambiguous plain-text type name |
<typeparam> | The role or constraint of a generic type parameter | Repeating the generic parameter name |
<inheritdoc /> | Documentation inherited from a base member or interface | Inheriting text when the implementation changes the contract |
Use cref and paramref for checked references
XML documentation can point to code elements instead of relying on prose that may drift:
/// <summary>
/// Loads an order with <see cref="IOrderRepository"/>.
/// </summary>
/// <param name="orderId">
/// The identifier passed to <see cref="IOrderRepository.FindAsync"/>.
/// </param>
public Task<Order?> LoadAsync(Guid orderId)
The generator can warn when a cref target or <param> name is invalid. That makes these tags safer than manually typing “see the FindAsync method” into a paragraph.
Put the contract in comments, not the implementation
Write what a caller needs to know:
- accepted values and units;
- observable side effects;
- authorization or state requirements;
- return states;
- exceptions the caller can respond to;
- ordering, retry, or concurrency behavior when it affects use.
Do not narrate every line of the method. A comment such as “loops through orders and adds each amount” becomes false after a refactor and reveals nothing that the code does not already show.
Enable XML documentation output
Add GenerateDocumentationFile to the project file:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Then run:
dotnet build
The build writes an XML documentation file beside the compiled assembly. Exact output paths depend on the target framework and build configuration.
Once generation is enabled, a project with undocumented public members may produce CS1591 warnings. Do not immediately suppress the warning across the entire project. First decide what the public API is.
Choose the surface you intend to document
For a reusable package, public types and members are the customer-facing API and usually deserve complete C# documentation. For an application, many public classes exist only because of framework conventions. Teams can still document them, but the useful unit may be the module, endpoint, job, or workflow rather than every property.
A practical policy is:
- document externally consumed public APIs strictly;
- document non-obvious internal contracts and failure modes;
- avoid comments that merely translate syntax into English;
- treat missing documentation warnings as errors only in the projects where the policy is appropriate.
If you need a narrow suppression, explain its scope in the project configuration or analyzer rules. A blanket NoWarn entry can make the build quiet while leaving the API unexplained.
A complete C# documentation example
The following interface shows how the common tags work together:
public interface IInvoiceService
{
/// <summary>
/// Creates a draft invoice for an existing customer.
/// </summary>
/// <param name="customerId">The customer that will own the invoice.</param>
/// <param name="lines">One or more invoice lines in display order.</param>
/// <param name="cancellationToken">Cancels the pending operation.</param>
/// <returns>The persisted draft, including its generated identifier.</returns>
/// <exception cref="CustomerNotFoundException">
/// No customer exists for <paramref name="customerId"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="lines"/> is empty or contains a non-positive quantity.
/// </exception>
Task<Invoice> CreateDraftAsync(
Guid customerId,
IReadOnlyList<InvoiceLine> lines,
CancellationToken cancellationToken = default);
}
An implementation that preserves the interface contract can use <inheritdoc />:
public sealed class InvoiceService : IInvoiceService
{
/// <inheritdoc />
public async Task<Invoice> CreateDraftAsync(
Guid customerId,
IReadOnlyList<InvoiceLine> lines,
CancellationToken cancellationToken = default)
{
// Implementation omitted.
}
}
If the implementation adds an observable constraint, write a complete comment or add an accurate <remarks> section. Do not hide a changed contract behind inherited documentation.
Make C# documentation verifiable
Documentation comments sit close to the code, but proximity does not guarantee accuracy. Add checks that can fail before stale documentation ships.
Compile examples where possible
Move substantial examples into sample projects or tests, then reference them from the documentation. A copied code fence cannot tell you when a type is renamed or a constructor changes.
Build the documentation in CI
Run dotnet build with XML documentation enabled. If you publish reference pages with DocFX or another generator, build those pages in the same pipeline and fail on broken references.
Review public API changes with their comments
When a pull request changes a parameter, exception, return state, or side effect, update its XML documentation in the same review. A spelling-only review will not catch a contract mismatch.
Check the rendered output
Raw XML comments are source material. Before release, inspect the actual IntelliSense text or generated reference page. Look for broken links, collapsed paragraphs, malformed lists, missing type names, and examples that lost formatting.
XML comments are only one layer
XML documentation is strong at member-level reference. It is weak at explaining how several assemblies, services, queues, and data stores work together. A maintainable C# documentation set usually includes:
- XML comments for public contracts and non-obvious behavior;
- a README or quickstart for setup and the first successful run;
- architecture pages for boundaries and dependencies;
- API documentation for externally callable endpoints;
- runbooks for deployment and failure recovery;
- decision records for choices that source code cannot explain.
For more examples, see our guide to C# XML comments and these code documentation best practices.
DocuWriter can analyze a connected repository and generate a structured starting point across architecture, modules, services, classes, functions, dependencies, APIs, diagrams, and READMEs. Teams can centralize those pages in Spaces and use Autopilot to help keep them current. XML comments remain valuable source evidence, and a maintainer should review generated pages for business rules and intent that are not explicit in the code.
C# documentation checklist
Before merging a C# documentation change, check that:
- each comment describes the public contract rather than the method body;
- every parameter name matches the signature;
- limits, units, nullable states, exceptions, and side effects are explicit;
creflinks resolve;- examples compile against the current API;
- the XML documentation file builds without unexpected warnings;
- the rendered reference is readable;
- broader architecture and workflow information lives outside member comments.
Good C# documentation is testable information attached to a real contract. Start with the members developers call most often, describe the decisions they need to make, and let the build catch what it can.