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.
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);
}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.
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.
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;
}
}