Getting started

edit

Generate the API file on build

Add the Meziantou.Framework.PublicApiGenerator.MSBuild package to the project of your library, and set the path of the API file in PublicApiGeneratorOutputPath:

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

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

<ItemGroup>
  <!-- The API file describes the library: it is not part of it -->
  <Compile Remove="PublicApi/**" />
</ItemGroup>
  • PrivateAssets="all" keeps the package out of the dependencies of your own package.
  • The path is relative to the project directory. The project compiles every .cs file of its directory, so exclude the API file with Compile Remove, or write it outside of the project directory. Otherwise the next build compiles the stubs with your code, and fails because every type is declared twice.

Build the project:

dotnet build

After the build, PublicApi/PublicApi.g.cs lists the public API of the assembly. For this code:

namespace Demo;

public enum Unit
{
    Celsius,
    Fahrenheit,
    Kelvin = 10,
}

public sealed class Thermometer : IComparable<Thermometer>
{
    private double _value;

    public Thermometer(string name) => Name = name;

    public string Name { get; private set; }
    public string? Location { get; set; }
    public event EventHandler? Changed;

    public double Read(Unit unit = Unit.Celsius) => _value;
    public void Rename(string name) { Name = name; Changed?.Invoke(this, EventArgs.Empty); }
    internal void Calibrate(double value) => _value = value; // Not visible outside the assembly
    public int CompareTo(Thermometer? other) => 0;
}

The API file is:

// Target Frameworks: net10.0
#nullable enable

namespace Demo
{
    public sealed class Thermometer : System.IComparable<global::Demo.Thermometer>
    {
        public string? Location { get => throw null; set { } }
        public string Name { get => throw null; }
        public event System.EventHandler? Changed;
        public Thermometer(string name) { }
        public int CompareTo(global::Demo.Thermometer? other) => throw null;
        public double Read(global::Demo.Unit unit = global::Demo.Unit.Celsius) => throw null;
        public void Rename(string name) { }
    }

    public enum Unit
    {
        Celsius = 0,
        Fahrenheit = 1,
        Kelvin = 10,
    }
}

The private field, the private setter and the internal method are not part of the public API, so they are not in the file. Method bodies are replaced by stubs, and the declarations are sorted. The API files describes the content of the files.

The file starts with a // <auto-generated/> comment, omitted in this example. The options remove it and split the API into several files.

Commit the API file

Commit the API file with the code. From then on, a pull request that changes the public API also changes this file, and its diff shows the change of the API, whatever the size of the change of the code.

Check the API file in CI

The file is written by the build, so a contributor who did not build before committing can forget to update it. In CI, check it instead of writing it:

dotnet build -p:PublicApiGeneratorVerifyNoChangeOnBuild=true

In this mode, the build writes nothing, and fails when the API file is out of date. To fix it, build locally and commit the new file. See Check the API files in CI.

Next steps