The API files
editAn API file is a C# file that declares the public API of an assembly, with stubs in place of the bodies. For this code:
namespace Demo;
public enum Unit
{
Celsius,
Fahrenheit,
Kelvin = 10,
}
public sealed class Thermometer : IComparable<Thermometer>
{
private double _value;
public Thermometer(string name) => Name = name;
public string Name { get; private set; }
public string? Location { get; set; }
public event EventHandler? Changed;
public double Read(Unit unit = Unit.Celsius) => _value;
public void Rename(string name) { Name = name; Changed?.Invoke(this, EventArgs.Empty); }
internal void Calibrate(double value) => _value = value; // Not visible outside the assembly
public int CompareTo(Thermometer? other) => 0;
}The generator writes:
// Target Frameworks: net10.0
#nullable enable
namespace Demo
{
public sealed class Thermometer : System.IComparable<global::Demo.Thermometer>
{
public string? Location { get => throw null; set { } }
public string Name { get => throw null; }
public event System.EventHandler? Changed;
public Thermometer(string name) { }
public int CompareTo(global::Demo.Thermometer? other) => throw null;
public double Read(global::Demo.Unit unit = global::Demo.Unit.Celsius) => throw null;
public void Rename(string name) { }
}
public enum Unit
{
Celsius = 0,
Fahrenheit = 1,
Kelvin = 10,
}
}What a file contains
- A header: a
// <auto-generated/>comment (omitted in the example above, see the options), the target frameworks of the assembly, then#nullable enable. - The public API: the types and members that are visible outside the assembly (
public,protectedandprotected internal), and the explicit implementations of public interfaces. Private andinternalmembers, accessors that are not visible (such as aprivate set), static constructors and compiler-generated types are not in the file. - The details of each declaration: modifiers, nullable annotations, default values of parameters, generic constraints, values of the enum members, and attributes such as
[SupportedOSPlatform("windows")]. - Stubs: a method or a property getter that returns a value is
=> throw null;, the others are{ }. - Fully qualified type names: the file has no
usingdirective, so a type name never depends on the context.global::is added where a name could be ambiguous.
The new modifier is not stored in the assembly: the generator adds it to the members that hide an inherited member, using the rules of the C# compiler. See The new modifier.
Extension members
The members of C# 14 extension blocks are written in extension blocks, as in the source code. For this code:
namespace Demo;
public static class StringExtensions
{
extension(string value)
{
public bool IsBlank => string.IsNullOrWhiteSpace(value);
public string Repeat(int count) => string.Concat(Enumerable.Repeat(value, count));
}
extension(string)
{
public static string Concatenate(params IEnumerable<string> values) => string.Concat(values);
}
}The generator writes:
// Target Frameworks: net10.0
#nullable enable
namespace Demo
{
public static class StringExtensions
{
public static string Repeat(this string value, int count) => throw null;
extension(string value)
{
public bool IsBlank { get => throw null; }
}
extension(string)
{
public static string Concatenate(params System.Collections.Generic.IEnumerable<string> values) => throw null;
}
}
}- Each member has its own block, after the other members of the class, so that the output does not depend on how you group the members in your code. The blocks are sorted by the type of the receiver, then as the other members.
- Static members, properties and operators are in their block, with the receiver as declared (
extension(string)for a block that only has static members). The methods the compiler generates for them, such asget_IsBlank(string), are not in the file. - Instance methods are written as classic extension methods, with a
thisparameter: the compiler generates this method for them, and it declares the same API.
File layouts
The API can be written to one file or split into several files. The layout is an option of the MSBuild package, of the tool and of the library:
| Layout | Files |
|---|---|
SingleFile (default) | One file for the whole assembly. The tool and the library name it <AssemblyName>.cs; the MSBuild package writes it at the path you set. |
OneFilePerNamespace | One file per namespace: Demo.g.cs, and GlobalNamespace.g.cs for the types that have no namespace. |
OneFilePerType | One file per top-level type, named after its full name: Demo.Thermometer.g.cs. Nested types are in the file of their containing type. |
When the assembly has a [RequiresPreviewFeatures] attribute, the OneFilePerNamespace and OneFilePerType layouts also write an AssemblyInfo.g.cs file that declares it. When two files would have the same name, ignoring case (for instance for the types Demo.Foo and Demo.foo), the generation fails instead of losing part of the API: use another layout.
A single file is the easiest to review and to check. The other layouts make diffs easier to read for a large API, but the files of the types or namespaces you remove stay on disk: see Removed types leave files behind.
Order of the declarations
The output only depends on the public API: moving a declaration in the source code, or to another file of a partial type, does not change it.
- Namespaces and types are sorted by name.
- Members are grouped by kind: fields, properties and indexers, events, constructors, destructors, methods, operators, then conversion operators. In a group, members are sorted by name, then by number of generic parameters, number of parameters, and types of the parameters. An explicit interface implementation is sorted by the name of the member, then by the name of the interface.
- Enum members are sorted by value, then by name.
- Interfaces, attributes and named arguments of attributes are sorted by name. The constraints of a generic parameter are sorted by name, after the base type.
All comparisons are ordinal.
Multi-targeted libraries
The builds of a library for several target frameworks are merged into one set of files. The declarations that are not the same in every target framework are wrapped in #if blocks, using the preprocessor symbol of each target framework (NET10_0, NETSTANDARD2_0...). For this code, built for netstandard2.0 and net10.0:
namespace Demo;
public readonly struct Temperature : IFormattable
#if NET10_0_OR_GREATER
, ISpanFormattable
#endif
{
public Temperature(double celsius) => Celsius = celsius;
public double Celsius { get; }
public string ToString(string? format, IFormatProvider? formatProvider) => Celsius.ToString(format, formatProvider);
#if NET10_0_OR_GREATER
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
=> Celsius.TryFormat(destination, out charsWritten, format, provider);
#endif
}The generator writes:
// Target Frameworks: net10.0, netstandard2.0
#nullable enable
namespace Demo
{
public readonly struct Temperature : System.IFormattable
#if NET10_0
, System.ISpanFormattable
#endif
{
public double Celsius { get => throw null; }
public Temperature(double celsius) { }
public string ToString(string? format, System.IFormatProvider? formatProvider) => throw null;
#if NET10_0
public bool TryFormat(System.Span<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) => throw null;
#endif
}
}Conditional blocks are kept as narrow as possible, so that adding a target framework produces a small diff. A type stays unconditional when the target frameworks only differ by its attributes, its members or its base type list: only the parts that are not shared by every target framework are wrapped. The whole type is wrapped only when it cannot be merged, for instance when it does not exist in every target framework, or when its modifiers or generic constraints differ.
The MSBuild package merges all the target frameworks of the project automatically. With the tool and the library, pass the build of each target framework.
Compiling the files
The files are valid C#, so you can compile them, for instance to check that they are consistent. Two settings of the project that compiles them matter:
- Nullable annotations: the files start with
#nullable enable. - Memory safety rules: for an assembly compiled with the updated memory safety rules (
<Features>$(Features);updated-memory-safety-rules</Features>), theunsafemodifier is written on the members the compiler marked as requiring an unsafe context, whatever their signature: pointers in a signature no longer implyunsafe, and delegate declarations are neverunsafe. Compiling these files may need the same feature. For other assemblies, a member isunsafewhen a pointer type appears in its signature, which matches the compatibility mode of the compiler.
Do not compile the API files into the library they describe: every type would be declared twice. See Getting started.