Generate and review the public API of a .NET library

 
 
  • Gérald Barré

When maintaining a library, one of the easiest ways to introduce a breaking change is to update a public type without noticing its impact on consumers.

You can use the Microsoft.CodeAnalysis.PublicApiAnalyzers analyzer to generate a file with the list of members. However, I don't like it as the members are not in C# syntax. Also, if you don't have the same public API for all target frameworks, you get multiple files that are hard to review. Also, it may not keep all metadata such as attributes, which can be important for consumers.

This post shows how to use Meziantou.Framework.PublicApiGenerator to generate a public API file from DLLs or loaded assemblies.

#What's the public API generator?

Meziantou.Framework.PublicApiGenerator generates compilable C# files that represent the public surface of a .NET assembly. The output is not meant to run. It is meant to be reviewed.

That distinction matters. Instead of inspecting metadata in a decompiler or guessing from a release note, you get a normalized snapshot of the API in familiar C# syntax. A pull request diff immediately shows when a method signature changes, when a nullable annotation is added or removed, or when a consumer-facing attribute such as ObsoleteAttribute appears.

The generator is also useful for multi-targeted packages. It can read multiple assemblies and merge them into a single reviewable output, so framework-specific differences stay visible instead of being hidden in separate build artifacts.

In practice, the generated file becomes a small contract that lives next to your source code. If the file changes, reviewers know the public API changed too, and they can decide whether that change is intentional and whether it should affect versioning.

The generated file looks like this:

C#
// <auto-generated/>
#nullable enable

namespace Meziantou.Framework
{
    public readonly struct RelativeDate : System.IComparable, System.IComparable<Meziantou.Framework.RelativeDate>, System.IEquatable<Meziantou.Framework.RelativeDate>, System.IFormattable
    {
        public RelativeDate(System.DateTime dateTime, System.TimeProvider? timeProvider) { }
        public RelativeDate(System.DateTime dateTime) { }
        public static Meziantou.Framework.RelativeDate Get(System.DateTime dateTime) => throw null;
        public static Meziantou.Framework.RelativeDate Get(System.DateTimeOffset dateTime) => throw null;
        public static Meziantou.Framework.RelativeDate Get(System.DateTime dateTime, System.TimeProvider? timeProvider) => throw null;
        public static Meziantou.Framework.RelativeDate Get(System.DateTimeOffset dateTime, System.TimeProvider? timeProvider) => throw null;
        public override string ToString() => throw null;
        public string ToString(string? format, System.IFormatProvider? formatProvider) => throw null;
        int System.IComparable.CompareTo(object? obj) => throw null;
        public int CompareTo(Meziantou.Framework.RelativeDate other) => throw null;
        public override bool Equals(object? obj) => throw null;
        public bool Equals(Meziantou.Framework.RelativeDate other) => throw null;
        public override int GetHashCode() => throw null;

        // code omitted for brevity
    }
}

#How to generate the public API file

##Method 1: Use the .NET tool to generate an API file

Install the tool:

Shell
dotnet tool install --global Meziantou.Framework.PublicApiGenerator.Tool

Generate the API from a compiled assembly:

Shell
Meziantou.Framework.PublicApiGenerator.Tool \
    --input "net8.0/Meziantou.Framework.RelativeDate.dll" \
    --input "net10.0/Meziantou.Framework.RelativeDate.dll" \
    --output ref/

You can commit it to your repository. In later pull requests, regenerate it and review the diff like any other code change.

##Method 2: Use the MSBuild task to generate an API file

If you want API generation to run automatically during your build, you can use the Meziantou.Framework.PublicApiGenerator.MSBuild package.

Install the package in the project:

Shell
dotnet package add Meziantou.Framework.PublicApiGenerator.MSBuild

Then configure the package in the project file. PublicApiGeneratorOutputPath is required.

XML
<ItemGroup>
    <PackageReference Include="Meziantou.Framework.PublicApiGenerator.MSBuild" Version="x.y.z" PrivateAssets="all" />
</ItemGroup>

<PropertyGroup>
    <PublicApiGeneratorOutputPath>ref/PublicApi.g.cs</PublicApiGeneratorOutputPath>

    <!-- Optional -->
    <PublicApiGeneratorGenerateOnBuild>true</PublicApiGeneratorGenerateOnBuild>
    <PublicApiGeneratorVerifyNoChangeOnBuild>false</PublicApiGeneratorVerifyNoChangeOnBuild>
    <PublicApiGeneratorFileLayout>SingleFile</PublicApiGeneratorFileLayout>
</PropertyGroup>

In CI, you can validate that the generated files are up to date without writing them:

Shell
dotnet build -p:PublicApiGeneratorVerifyNoChangeOnBuild=true

##Method 3: Use the library

You can also use the library, for example to validate API stability from tests. The library support both DLL files and in-memory assemblies, so you can generate the API from a compiled assembly or directly from your source code.

Shell
dotnet package add Meziantou.Framework.PublicApiGenerator
C#
using Meziantou.Framework.PublicApiGenerator;

var options = new PublicApiGeneratorOptions();

var result = PublicApiGenerator.GeneratePublicApi(typeof(MyLibrary.EntryPoint).Assembly, options);

// You can use Snapshot testing to compare with your approved baseline (snapshot, file, or inline expected text)
Snapshot.Validate(result);

This pattern works well with [snapshot testing because every intentional API change is explicit in the test diff.

#CI validation

You can fail the CI build when the public API file is out of date.

If you use the CLI tool, run it in verification mode:

Shell
Meziantou.Framework.PublicApiGenerator.Tool \
    --input "net8.0/Meziantou.Framework.RelativeDate.dll" \
    --input "net10.0/Meziantou.Framework.RelativeDate.dll" \
    --output ref/ \
    --verify-no-change

If you use the MSBuild package, enable verification during build:

Shell
dotnet build -p:PublicApiGeneratorVerifyNoChangeOnBuild=true

Both options are useful in pull request workflows. Developers regenerate the API file locally when the change is intentional, and CI ensures unintentional API changes are detected before merge.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?