meetings/working-groups/extensions/Extension-API-docs.md
Extensions introduce new requirements for our API reference pipeline. The addition of new extension member types, and the representation of extension containers require changes to the pipeline for a good customer experience:
The new extensions experience should be built on the framework used for the existing extension methods. In fact, when a new extension member is a method whose receiver is an instance, both forms are binary compatible. The document describes the new experience as a set of enhancements to the existing extension method documentation.
The prototype for an extension method communicates many of the key concepts that consumers need to use these methods in their application. Consider this prototype:
public static class SomeExtensionsAndStuff
{
public static bool IsEmpty<T>(this IEnumerable<T> source) => source.Any() == false;
}
The prototype and the class declaration communicate important information to readers.
this modifier indicates two important keys:
IEnumerable<T>.SomeExtensionsAndStuff indicates how it can be called as a static method, if multiple extension methods have signatures that create an ambiguity.For example, users can call extension methods in two ways:
bool empty = sequence.IsEmpty();
bool empty = SomeExtensionsAndStuff.IsEmpty(sequence);
The presentation and navigation elements used for the docs site help users find these methods and recognize that these methods are extension methods. For the following notes the links are to the docs for System.Linq.Enumerable and the extensions on System.Collections.Generic.IEnumerable<T> as they exist today:
IEnumerable<T>, list only the members defined on the interface. In other words, none of the extension methods are listed in the TOC (left navigation pane) under the extended type.IEnumerable<T>, that lists all extension methods. This section uses the following format:
this modifier, indicating that they are extension methods.Sum(this IEnumerable<int>)) or generic (Where<T>(this IEnumerable<T> source, Func<T, bool> predicate))./// comments on the extended type (for example, System.Collections.Generic.IEnumerable<T>) doesn't need to include all extension methods in the entire library.In source, the existing /// elements on the extending type and the extension method declarations enable this presentation.
The presentation for C# 14 extensions needs to account for several new types of extension members:
This proposal currently assumes no changes to the presentation for existing extension methods.
Alternative: We could experiment with displaying all extension members using this updated format. Readers can give feedback on whether they prefer a unified presentation, or want to know if a method follows the new or old format. There is no reason a consumer needs to know which format was used to declare an extension. The declarations are binary compatible.
When an extension member prototype is shown, the format should show the extension container:
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty { get; }
}
The source parameter is referred to as the receiver parameter, or receiver. The receiver parameter may be an instance, as shown above, or it may be a type, as in the following:
extension<T>(IEnumerable<T>)
{
public static IEnumerable<T> Create(int size, Func<int, T> factory);
}
Alternatives: The prototypes above show a single extension member in an extension container. In source, multiple members share the same receiver and are declared in the same extension container. An alternative for the prototypes would be as follows:
csharpextension<T>(IEnumerable<T> source) { public bool IsEmpty { get; } public int Length { get; } public IEnumerable<T> Sample(int sampleInterval); }It could save screen space to collect members by receiver declaration and remove the duplication. That would only apply on pages where multiple prototypes are shown together. Another negative is that it could conflict with the current lists in the TOC that have all overloads collected together. We should specify the URL for the docs page for a single
extensiondeclaration.
The receiver parameter includes a parameter name when the receiver is an instance. The receiver parameter doesn't include a parameter name when the receiver is a type.
This presentation enables the following:
this modifier on the first parameter).extension container indicates that the member is an extension member.extension node indicates the type extended, and provides a key to know if the member is intended to extend an instance or extend the type.static modifier if it would be present in source, as in an operator declaration:
extension<T>(IEnumerable<T>)
{
public static IEnumerable<T> operator + (IEnumerable<T> left, IEnumerable<T> right);
}
The API docs build system generates the section on the type page for the extended type that lists all extension members. This section should have sub-sections for extension methods, extension properties, and extension operators. Extension indexers should follow the format for indexers, and be listed as an Item[] property. There isn't a this modifier on the first parameter. In fact, the receiver is declared on the extension, not the member. The prototypes in this section should expand to show the extension container, as follows:
extension(IEnumerable<int> source) { ... }) or generic (extension<T>(IEnumerable<T> source) { ... }).The page for the class containing extensions will need only minimal updates in how extension members are displayed. The static classes that contain extension methods are classes, and could already define static properties, indexers, and operators. The additional work involves understanding the unspeakable extension type that contains new extension members.
There should be a new style for extension members. This should be modeled after the existing member template, with the following changes:
The emphasis on the receiver parameter reinforces the new syntax, and is necessary for readers to see the extended type on the new extension member.
The compiler generates a public skeleton class that defines prototypes for extension members. The skeleton class has an unspeakable name and contains the following prototypes:
<Extension>. This private static method includes one parameter that represents the receiver for the extension members declared in the skeleton.The unspeakable skeleton provides the prototypes for the extension members and the receiver type. The nodes of the skeleton provide a location for the XML output from the /// comments on the extension members and the receiver parameter. The /// comments on the extension declaration are written as XML on the node for the unspeakable member declaring the receiver. The /// comments on each extension member are written as XML on the node for the embedded member of the unspeakable containing class.
See the following code and XML for an example of extension members and the resulting XML output.
/// <summary>Summary for E</summary>
static class E
{
/// <summary>Summary for extension block</summary>
/// <typeparam name="T">Description for T</typeparam>
/// <param name="t">Description for t</param>
extension<T>(T t)
{
/// <summary>Summary for M</summary>
/// <typeparam name="U">Description for U</typeparam>
/// <param name="u">Description for u</param>
public void M<U>(U u) => throw null!;
/// <summary>Summary for P</summary>
public int P => 0;
}
}
produces the following XML output:
<?xml version="1.0"?>
<doc>
<assembly>
<name>Test</name>
</assembly>
<members>
<member name="T:E">
<summary>Summary for E</summary>
</member>
<member name="T:E.<>E__0`1">
<summary>Summary for extension block</summary>
<typeparam name="T">Description for T</typeparam>
<param name="t">Description for t</param>
</member>
<member name="M:E.<>E__0`1.M``1(``0)">
<summary>Summary for M</summary>
<typeparam name="U">Description for U</typeparam>
<param name="u">Description for u</param>
</member>
<member name="P:E.<>E__0`1.P">
<summary>Summary for P</summary>
</member>
<member name="M:E.M``2(``0,``1)">
<inheritdoc cref="M:E.<>E__0`1.M``1(``0)"/>
</member>
<member name="M:E.get_P``1(``0)">
<inheritdoc cref="P:E.<>E__0`1.P"/>
</member>
</members>
</doc>
/// commentsThe compiler uses the skeleton declarations to produce the XML output for all /// comments on the extension node and the embedded extension members:
extension block are applied to the embedded receiver field in the unspeakable nested class. This can include type parameter information and parameter information for the receiver.typeparamref or paramref nodes in the extension block resolve to the corresponding typeparam and param nodes on the extension block.<inheritdoc cref="skeleton-member" /> node to point to the generated XML output from the skeleton member.The nodes on the receiver and each member must be merged by the tools that consume the XML (for example, Visual Studio IntelliSense, or the MS Learn build process).
A disambiguation syntax is required when more than one class declares extension members with the same signature. Consumers must use a static syntax to specify which method should be called. We believe this is the less common case. However, it is common enough that our docs presentation should clearly display the class where an extension member is declared.
The C# LDM hasn't finalized the disambiguation syntax. The final disambiguation syntax shouldn't impact the API generation pipeline. We will demonstrate it in docs.