Meziantou.Framework.PublicApiGenerator
2.0.18
Prefix Reserved
dotnet add package Meziantou.Framework.PublicApiGenerator --version 2.0.18
NuGet\Install-Package Meziantou.Framework.PublicApiGenerator -Version 2.0.18
<PackageReference Include="Meziantou.Framework.PublicApiGenerator" Version="2.0.18" />
<PackageVersion Include="Meziantou.Framework.PublicApiGenerator" Version="2.0.18" />
<PackageReference Include="Meziantou.Framework.PublicApiGenerator" />
paket add Meziantou.Framework.PublicApiGenerator --version 2.0.18
#r "nuget: Meziantou.Framework.PublicApiGenerator, 2.0.18"
#:package Meziantou.Framework.PublicApiGenerator@2.0.18
#addin nuget:?package=Meziantou.Framework.PublicApiGenerator&version=2.0.18
#tool nuget:?package=Meziantou.Framework.PublicApiGenerator&version=2.0.18
Meziantou.Framework.PublicApiGenerator
Meziantou.Framework.PublicApiGenerator generates compilable C# files that represent the public API of a .NET assembly.
It can read metadata directly from a .dll or .exe (via PEReader and MetadataReader) or from a loaded Assembly. Both paths produce the same model and deterministic output.
It also exposes a structured model of the public API (types, members, signatures, attributes, XML documentation IDs and metadata provenance), which can be used without generating C# files. See Reading the API model.
Usage
The types are in the Meziantou.Framework.PublicApiGenerator namespace. This example generates the public API of Meziantou.Framework.PublicApiGenerator.dll itself:
var files = PublicApi.Generate(
assemblyPath: typeof(PublicApi).Assembly.Location,
options: new PublicApiOptions
{
FileLayout = PublicApiFileLayout.OneFilePerNamespace,
IncludeAutoGeneratedComment = false,
});
// The assembly declares a single namespace
var file = Assert.Single(files);
Assert.Equal("Meziantou.Framework.PublicApiGenerator.g.cs", file.RelativePath);
Assert.Contains("public static class PublicApi", file.Content);
Multi-target usage
You can merge multiple target frameworks into one output and emit conditional compilation blocks for target-specific differences:
var files = PublicApi.Generate(
assemblySources:
[
"/path/to/MyLibrary.netstandard2.0.dll",
"/path/to/MyLibrary.net10.0.dll",
]);
If needed, you can provide explicit framework monikers for specific entries:
var files = PublicApi.Generate(
assemblySources:
[
new AssemblySource("/path/to/MyLibrary.netstandard2.0.dll", ".NETStandard,Version=v2.0"),
new AssemblySource("/path/to/MyLibrary.net10.0.dll", ".NETCoreApp,Version=v10.0"),
]);
If TargetFrameworkMoniker is omitted (or empty), the generator infers it from assembly metadata.
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 their attributes, their members or their base type list; only the parts that are not shared by every target framework are wrapped in a conditional block:
public sealed class CronExpression : IRecurrenceRule
#if NET10_0
, System.IParsable<CronExpression>
#endif
{
public static CronExpression Parse(string expression) => throw null;
#if NET10_0
public static CronExpression Parse(System.ReadOnlySpan<char> expression) => throw null;
#endif
}
The whole type is wrapped in a conditional block 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.
Order of the declarations
The output only depends on the public API: moving a declaration in the source code, or in 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. The model (PublicApiType.Members, PublicApiSymbol.Attributes...) uses the same order.
Reading the API model
PublicApi.ReadAssembly returns a structured model of the public API, without generating C# files. It reads the metadata of the file or stream: the assembly, its attributes and its references are never loaded or executed. The model does not keep any reference to the file, the stream or the metadata reader, so it remains usable after they are disposed.
This example lists the methods of Meziantou.Framework.PublicApiGenerator.dll and their parameters:
var assembly = PublicApi.ReadAssembly(typeof(PublicApi).Assembly.Location, new PublicApiReadOptions
{
TargetFramework = "net10.0", // Optional, inferred from TargetFrameworkAttribute
InputIdentity = "Meziantou.Framework.PublicApiGenerator/1.0.0/lib/net10.0/Meziantou.Framework.PublicApiGenerator.dll", // Optional, see "Identities" below
});
var lines = new List<string>();
foreach (var type in assembly.GetAllTypes()) // Includes nested types
{
lines.Add($"{type.FullName} ({type.TypeKind}), declared in {type.DeclaringType?.FullName ?? type.Namespace}");
foreach (var member in type.Members)
{
if (member is PublicApiMethod method)
{
lines.Add($" {method.Name} returns {method.ReturnType}");
foreach (var parameter in method.Parameters)
{
lines.Add($" {parameter.RefKind} {parameter.Type} {parameter.Name} (nullable: {parameter.Type.NullableAnnotation})");
}
}
}
}
Assert.Contains("Meziantou.Framework.PublicApiGenerator.PublicApi (Class), declared in Meziantou.Framework.PublicApiGenerator", lines);
Assert.Contains(" ReadAssembly returns Meziantou.Framework.PublicApiGenerator.PublicApiAssembly", lines);
Assert.Contains(" None string assemblyPath (nullable: NotAnnotated)", lines);
Assert.Contains(" None Meziantou.Framework.PublicApiGenerator.PublicApiReadOptions? options (nullable: Annotated)", lines);
The model contains:
- The types and members that are visible outside the assembly (
public,protected,protected internal), and explicit implementations of interfaces that are visible outside the assembly. Members with names that cannot be written in C# (e.g.<Clone>$), static constructors and compiler-generated types are excluded. Other compiler-generated members, such as the members of records, are included and can be identified usingIsCompilerGenerated. - Accessors, available from
PublicApiProperty.GetMethod,SetMethod,PublicApiEvent.AddMethod... Accessors that are not visible outside the assembly (e.g. aprivate set) are not exposed. - The signature of delegates through
PublicApiType.DelegateInvokeMethod. The other members of delegates are not exposed. - C# 14 extension blocks. The static methods the compiler generates for them (e.g.
get_Length(string)) are members of the static class, andPublicApiType.ExtensionBlocksexposes the blocks as written in source: the receiver, the type parameters and the members (e.g. the propertyLength), with the compiler's documentation IDs.PublicApiMethod.ExtensionImplementationandImplementedExtensionMemberlink a member to its implementation, andPublicApiAggregatedSymbol.ExtensionMembersaggregates the members across target frameworks. - All the custom attributes of each symbol, parameter, return value and generic parameter, including the ones that are not written in the generated C# files (e.g.
EditorBrowsableAttributeorNullableAttribute), with their decoded arguments. - Type references (
PublicApiTypeReference) that preserve nested types, type arguments, arrays, pointers, function pointers, ref kinds, nullable annotations, tuple element names and the referenced assembly. - The
newmodifier of the members and nested types that hide an inherited member (PublicApiMember.IsNewandPublicApiType.IsNew). See Thenewmodifier.
Internal symbols
Set PublicApiReadOptions.IncludeInternalSymbols to true to also read the internal types and members, for instance to document an assembly compiled from the C# files of a source package, whose types are usually internal:
- The model also contains the
internaltypes, members and accessors, the types nested ininternaltypes, theinternalinterfaces a type implements and the explicit implementations of their members. Accessibilityreports the declared accessibility (e.g.PublicApiAccessibility.Internal), soPublicApiFormatterwritesinternal(e.g.public int Value { get; internal set; }).- A member that hides an
internalmember inherited from a type declared in the same assembly is reported asnew. privateandprivate protectedsymbols are still excluded, and so are theinternaltypes generated by the compiler (e.g. the embeddedNullableAttribute,<PrivateImplementationDetails>or the types declared with thefilemodifier). Theinternalmembers of the types declared in other assemblies are ignored, even when they are visible throughInternalsVisibleToAttribute.
The new modifier
The new modifier is not stored in metadata, so it is inferred using the rules of the C# compiler: a member is reported as new when a base class, or a base interface for the members of an interface, declares a member with the same name, or with the same signature for methods and indexers.
public class Base
{
public void Method() { }
}
public class Derived : Base
{
public new int Method() => 0; // Hides Base.Method()
public void Method(int value) { } // Another overload, it does not hide anything
}
Only the inherited members that are visible outside their assembly (public, protected and protected internal) are considered. A member that hides an internal member is not reported as new, as the hidden member is not part of the public API, unless PublicApiReadOptions.IncludeInternalSymbols is set and the hidden member is declared in the same assembly.
When a base type is declared in another assembly, the metadata of that assembly is read to find the inherited members. The assembly is looked up in the directory of the assembly being read, then in the assemblies of the runtime that runs the generator. It is never loaded nor executed. When the assembly cannot be found, the members of that base type and of its own base types are ignored, so the members that hide them are not reported as new.
Documentation IDs, identities and metadata tokens
Each symbol exposes three kinds of identifiers:
DocumentationId: the XML documentation ID generated by the C# compiler (e.g.M:MyNamespace.MyType.MyMethod(System.Int32)). Use it to find the documentation of a symbol in the XML documentation file of the assembly, or to resolve acref. It is not unique across assemblies: the builds of an assembly for several target frameworks, or unrelated assemblies, can declare the same documentation ID.Identity: the documentation ID scoped to its assembly (PublicApiAssembly.Scope). The scope isPublicApiReadOptions.InputIdentitywhen it is provided; otherwise it is made of the assembly display name and of the module version ID (MVID), so it changes when the assembly is rebuilt with different inputs. It never contains a file path.Origin: the metadata token of the entity the symbol was read from, with the MVID of its module. Tokens are not stable across builds and are only meaningful for their module. Use them, withPublicApiModule.PdbReferences, to find the debug information (e.g. sequence points) of a symbol. Accessors have their own origin.
For instance, with the following type, declared in the PublicApiGeneratorTests class of the Meziantou.Framework.PublicApiGenerator.Tests namespace:
public sealed class MyType<T>
{
public List<string?> MyMethod(int value) => [];
public sealed class Nested;
}
var symbol = assembly.FindSymbolByDocumentationId("M:Meziantou.Framework.PublicApiGenerator.Tests.PublicApiGeneratorTests.MyType`1.MyMethod(System.Int32)");
var type = assembly.FindType("Meziantou.Framework.PublicApiGenerator.Tests.PublicApiGeneratorTests+MyType`1+Nested");
Formatting declarations
PublicApiFormatter formats a symbol or a type reference using the C# syntax:
var declaration = PublicApiFormatter.Format(symbol);
Assert.Equal("public System.Collections.Generic.List<string?> MyMethod(int value)", declaration.Text);
var references = new List<string>();
foreach (var segment in declaration.Segments)
{
if (segment.Kind == PublicApiDeclarationSegmentKind.TypeName && segment.TypeReference is PublicApiNamedTypeReference typeReference)
{
// e.g. generate a link to the documentation of the referenced type
references.Add($"{segment.Text} => {typeReference.DocumentationId} in {typeReference.AssemblyName}");
}
}
Assert.Equal(
[
"System.Collections.Generic.List => T:System.Collections.Generic.List`1 in System.Collections",
"string => T:System.String in System.Runtime",
"int => T:System.Int32 in System.Runtime",
],
references);
PublicApiDeclarationStyle.Declaration(default) writes a declaration suitable for display: no member body, no constructor initializer, no type body. Attributes are written before the declaration (useIncludeAttributesorAttributeFilterto change that), generic constraints after the signature, and properties list their accessors (e.g.public int Value { get; protected set; }). Type names are fully qualified unlessQualifyTypeNamesisfalse.PublicApiDeclarationStyle.Compilablewrites the stubs used byPublicApi.Generate(e.g.public int MyMethod(int value) => throw null;). Formatting a type writes the type with all its members and nested types.
Multi-target model
PublicApi.Aggregate combines the builds of an assembly for several target frameworks. Symbols are matched by documentation ID, and each symbol lists the target frameworks that declare it and its distinct declarations (variants). Each variant keeps the symbol of each of its target frameworks, with its own metadata provenance.
var aggregate = PublicApi.Aggregate(
[
PublicApi.ReadAssembly("/path/to/netstandard2.0/MyLibrary.dll", new PublicApiReadOptions { TargetFramework = "netstandard2.0" }),
PublicApi.ReadAssembly("/path/to/net10.0/MyLibrary.dll", new PublicApiReadOptions { TargetFramework = "net10.0" }),
]);
foreach (var type in aggregate.Types)
{
foreach (var member in type.Members)
{
if (!member.IsAvailableInAllTargetFrameworks)
Console.WriteLine($"{member.DocumentationId} is only available in {string.Join(", ", member.TargetFrameworks)}");
if (member.Differences != PublicApiSymbolDifferences.None) // e.g. Signature, Nullability, Constraints, Attributes, Inheritance
{
foreach (var variant in member.Variants)
Console.WriteLine($"{string.Join(", ", variant.TargetFrameworks)}: {PublicApiFormatter.Format(variant.Symbol)}");
}
}
}
The assemblies must have the same name and distinct target frameworks, otherwise Aggregate throws an ArgumentException. Declarations are compared using the structured model: two symbols with the same documentation ID belong to different variants when their return types, nullable annotations, constraints, modifiers, attributes, constant values, or base types differ.
Memory safety rules
For assemblies compiled with the updated memory safety rules (<Features>$(Features);updated-memory-safety-rules</Features>), the unsafe modifier reflects the members the compiler marked as requiring an unsafe context, whatever their signature. Pointers in a signature no longer imply unsafe, and delegate declarations are never unsafe. Generated files may therefore need the same feature to be compiled.
For other assemblies, a member is unsafe when a pointer type appears in its signature, which matches the compatibility mode of the compiler.
Example output
Input API:
namespace Demo;
public sealed class Sample
{
public string? Echo(string? value) => value;
}
Generated API:
// Target Frameworks: net10.0
#nullable enable
namespace Demo
{
public sealed class Sample
{
public string? Echo(string? value) => throw null;
}
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.18 | 46 | 11/13/2026 |
| 2.0.17 | 44 | 11/13/2026 |
| 2.0.16 | 89 | 11/8/2026 |
| 2.0.15 | 148 | 10/31/2026 |
| 2.0.14 | 193 | 10/29/2026 |
| 2.0.13 | 99 | 10/28/2026 |
| 2.0.12 | 113 | 10/21/2026 |
| 2.0.11 | 100 | 10/14/2026 |
| 2.0.10 | 105 | 10/11/2026 |
| 2.0.9 | 119 | 10/9/2026 |
| 2.0.8 | 109 | 9/27/2026 |
| 2.0.7 | 116 | 9/11/2026 |
| 2.0.6 | 122 | 9/6/2026 |
| 2.0.5 | 139 | 9/3/2026 |
| 2.0.4 | 127 | 8/29/2026 |
| 2.0.3 | 132 | 8/9/2026 |
| 2.0.2 | 131 | 7/12/2026 |
| 2.0.1 | 133 | 7/8/2026 |
| 2.0.0 | 144 | 7/5/2026 |
| 1.0.8 | 141 | 7/5/2026 |