Skip to main content
net10.0net11.0

Generate compilable C# public API stubs from .NET assemblies.

Published
Oct 11, 2026
7d ago
Total Downloads
1.9K
1,866

Install

dotnet add package Meziantou.Framework.PublicApiGenerator --version 2.0.10
ProductVersions
.NET
net10.0net11.0

README

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

using Meziantou.Framework.PublicApiGenerator;

var files = PublicApi.Generate(
    assemblyPath: "/path/to/MyLibrary.dll",
    options: new PublicApiOptions
    {
        FileLayout = PublicApiFileLayout.OneFilePerNamespace,
        IncludeAutoGeneratedComment = false,
    });

foreach (var file in files)
{
    Console.WriteLine(file.RelativePath);
    Console.WriteLine(file.Content);
}

Multi-target usage

You can merge multiple target frameworks into one output and emit conditional compilation blocks for target-specific differences:

using Meziantou.Framework.PublicApiGenerator;

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:

using Meziantou.Framework.PublicApiGenerator;

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.

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.

using Meziantou.Framework.PublicApiGenerator;

var assembly = PublicApi.ReadAssembly("/path/to/MyLibrary.dll", new PublicApiReadOptions
{
    TargetFramework = "net10.0",                          // Optional, inferred from TargetFrameworkAttribute
    InputIdentity = "MyLibrary/1.2.0/lib/net10.0/MyLibrary.dll", // Optional, see "Identities" below
});

foreach (var type in assembly.GetAllTypes()) // Includes nested types
{
    Console.WriteLine($"{type.FullName} ({type.TypeKind}), declared in {type.DeclaringType?.FullName ?? type.Namespace}");
    foreach (var member in type.Members)
    {
        if (member is PublicApiMethod method)
        {
            Console.WriteLine($"  {method.Name} returns {method.ReturnType}");
            foreach (var parameter in method.Parameters)
            {
                Console.WriteLine($"    {parameter.RefKind} {parameter.Type} {parameter.Name} (nullable: {parameter.Type.NullableAnnotation})");
            }
        }
    }
}

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 through the static methods the compiler generates for them (e.g. get_Length(string)).
  • 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.

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() => throw null; // 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.

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.
var symbol = assembly.FindSymbolByDocumentationId("M:MyNamespace.MyType.MyMethod(System.Int32)");
var type = assembly.FindType("MyNamespace.MyType`1+Nested");

Formatting declarations

PublicApiFormatter formats a symbol or a type reference using the C# syntax:

var declaration = PublicApiFormatter.Format(symbol);
Console.WriteLine(declaration.Text); // public System.Collections.Generic.List<string?> MyMethod(int value)

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
        Console.WriteLine($"{segment.Text} => {typeReference.DocumentationId} in {typeReference.AssemblyName}");
    }
}
  • 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;
    }
}

Changes from Previous Version

No structural changes between v2.0.9 and v2.0.10.

Adoption Assessment

A verified publisher signal is available. Assess fit from the package's supported frameworks, license and dependency graph rather than popularity alone.

Compatibility metadata lists 2 target frameworks; confirm the exact target used by your application.

Related Guides & Workflows

Learn how to evaluate, integrate, or audit this package in your .NET project.