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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Meziantou.Framework.PublicApiGenerator" Version="2.0.18" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Meziantou.Framework.PublicApiGenerator" Version="2.0.18" />
                    
Directory.Packages.props
<PackageReference Include="Meziantou.Framework.PublicApiGenerator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Meziantou.Framework.PublicApiGenerator --version 2.0.18
                    
#r "nuget: Meziantou.Framework.PublicApiGenerator, 2.0.18"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Meziantou.Framework.PublicApiGenerator@2.0.18
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Meziantou.Framework.PublicApiGenerator&version=2.0.18
                    
Install as a Cake Addin
#tool nuget:?package=Meziantou.Framework.PublicApiGenerator&version=2.0.18
                    
Install as a Cake Tool

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 using IsCompilerGenerated.
  • Accessors, available from PublicApiProperty.GetMethod, SetMethod, PublicApiEvent.AddMethod... Accessors that are not visible outside the assembly (e.g. a private 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, and PublicApiType.ExtensionBlocks exposes the blocks as written in source: the receiver, the type parameters and the members (e.g. the property Length), with the compiler's documentation IDs. PublicApiMethod.ExtensionImplementation and ImplementedExtensionMember link a member to its implementation, and PublicApiAggregatedSymbol.ExtensionMembers aggregates 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. EditorBrowsableAttribute or NullableAttribute), 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 new modifier of the members and nested types that hide an inherited member (PublicApiMember.IsNew and PublicApiType.IsNew). See The new modifier.

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 internal types, members and accessors, the types nested in internal types, the internal interfaces a type implements and the explicit implementations of their members.
  • Accessibility reports the declared accessibility (e.g. PublicApiAccessibility.Internal), so PublicApiFormatter writes internal (e.g. public int Value { get; internal set; }).
  • A member that hides an internal member inherited from a type declared in the same assembly is reported as new.
  • private and private protected symbols are still excluded, and so are the internal types generated by the compiler (e.g. the embedded NullableAttribute, <PrivateImplementationDetails> or the types declared with the file modifier). The internal members of the types declared in other assemblies are ignored, even when they are visible through InternalsVisibleToAttribute.

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 a cref. 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 is PublicApiReadOptions.InputIdentity when 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, with PublicApiModule.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 (use IncludeAttributes or AttributeFilter to 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 unless QualifyTypeNames is false.
  • PublicApiDeclarationStyle.Compilable writes the stubs used by PublicApi.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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
Loading failed