Install
| Product | Versions |
|---|---|
| .NET |
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 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 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.
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.
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 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.
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 (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;
}
}
Changes from Previous Version
Adoption Assessment
A verified publisher signal is available. Assess fit from the package's supported frameworks, license and dependency graph rather than popularity alone.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.