dotnet-api-surface-validation について
.NET ライブラリのメンテナー向けに、公開 API の検証と追跡方法をまとめています。ソース管理する API リスト、リフレクションによるスナップショット、CI での互換性確認を組み合わせ、変更をレビュー可能にします。
主な機能
- 01PublicApiAnalyzers による公開済み・未公開 API の追跡
- 02Verify によるアセンブリ API のスナップショット
- 03ApiCompat による CI での API 差分確認
- 04ターゲットフレームワーク別の API リスト
活用シーン
- 01開発中に未登録の公開メンバーを検出
- 02リリース時に API 変更を確認して記録
- 03プルリクエストで未承認の互換性変更を阻止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-api-surface-validationインストール後、Claude Code で/dotnet-api-surface-validationを入力して読み込まれたか確認します。
詳細
dotnet-api-surface-validation
.NET ライブラリのパブリック API サーフェスを検証および追跡するためのツールとワークフローです。3 つの相補的なアプローチを扱います。Roslyn 診断を使用してリリース済み/未リリース API をテキストファイルで追跡する PublicApiAnalyzers、リフレクションベースの API サーフェスのスナップショットテストを行う Verify スナップショットパターン、API サーフェスの変更があるプルリクエストをブロックする ApiCompat CI による適用 です。
バージョンに関する前提: .NET 8.0 以降を基準とします。PublicApiAnalyzers 3.3 以降(Microsoft.CodeAnalysis.Analyzers に含まれるか、単体の Microsoft.CodeAnalysis.PublicApiAnalyzers として提供されます)。ApiCompat ツールは .NET 8 以降の SDK に含まれています。
対象外: バイナリ互換性とソース互換性のルール、型フォワーダー、SemVer への影響については、[skill:dotnet-library-api-compat] を参照してください。NuGet パッケージ化、EnablePackageValidation の基本、抑制ファイルの仕組みについては、[skill:dotnet-nuget-authoring] と [skill:dotnet-multi-targeting] を参照してください。Verify ライブラリの基本(セットアップ、スクラビング、コンバーター)については、[skill:dotnet-snapshot-testing] を参照してください。一般的な Roslyn アナライザーの構成(EditorConfig、重大度レベル)については、[skill:dotnet-roslyn-analyzers] を参照してください。HTTP API のバージョン管理については、[skill:dotnet-api-versioning] を参照してください。
相互参照:[skill:dotnet-library-api-compat] はバイナリ互換性とソース互換性のルール、[skill:dotnet-nuget-authoring] は EnablePackageValidation と NuGet SemVer、[skill:dotnet-multi-targeting] は複数 TFM における ApiCompat ツールの仕組み、[skill:dotnet-snapshot-testing] は Verify の基本、[skill:dotnet-roslyn-analyzers] は一般的なアナライザーの構成、[skill:dotnet-api-versioning] は HTTP API のバージョン管理について説明しています。
PublicApiAnalyzers
PublicApiAnalyzers は、ソース管理にコミットされたテキストファイルで、すべてのパブリック API メンバーを追跡します。アナライザーは、新しい API を「リリース済み」とする前に明示的な「未リリース」段階を経るようにすることで、意図しないパブリック API の公開や、文書化されていないサーフェスの変更を防ぎます。
セットアップ
アナライザーパッケージをインストールします。
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.*" PrivateAssets="all" />
</ItemGroup>
プロジェクトのルート(.csproj と同じ場所)に、追跡用の 2 つのファイルを作成します。
MyLib/
MyLib.csproj
PublicAPI.Shipped.txt # APIs shipped in released versions
PublicAPI.Unshipped.txt # APIs added since last release
どちらのファイルも、空であっても作成する必要があります。それぞれにヘッダーコメントを含めてください。
#nullable enable
#nullable enable ヘッダーは、API シグネチャ内の nullable 注釈を追跡するようアナライザーに指示します。このヘッダーがない場合、nullable コンテキストの違いは無視されます。
診断ルール
| ルール | 重大度 | 意味 |
|---|---|---|
| RS0016 | 警告 | パブリック API メンバーが API 追跡ファイルに宣言されていません |
| RS0017 | 警告 | パブリック API メンバーが削除されましたが、追跡ファイルに残っています |
| RS0024 | 警告 | パブリック API メンバーの nullable 注釈が正しくありません |
| RS0025 | 警告 | リリース済みとマークされたパブリック API シンボルのシグネチャが変更されています |
| RS0026 | 警告 | PublicAPI.Unshipped.txt にエントリがないまま、新しいパブリック API が追加されました |
| RS0036 | 警告 | API ファイルに #nullable enable ヘッダーがありません |
| RS0037 | 警告 | パブリック API が宣言されていますが、ソースに存在しません |
RS0016 は最もよく発生する診断です。新しい public または protected メンバーを追加すると、そのシグネチャを PublicAPI.Unshipped.txt に追加するまで RS0016 が発生します。IDE のコード修正(電球アイコン)を使うと、エントリを自動的に追加できます。
RS0017 は、public メンバーを削除または名前変更したにもかかわらず、古いシグネチャが追跡ファイルに残っている場合に発生します。該当するファイルから古い行を削除してください。
ファイル形式
追跡ファイルの各行は、ドキュメントコメント ID 形式を使用して 1 つのパブリック API シンボルを表します。
#nullable enable
MyLib.Widget
MyLib.Widget.Widget() -> void
MyLib.Widget.Name.get -> string!
MyLib.Widget.Name.set -> void
MyLib.Widget.Calculate(int count) -> decimal
MyLib.Widget.CalculateAsync(int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<decimal>!
MyLib.IWidgetFactory
MyLib.IWidgetFactory.Create(string! name) -> MyLib.Widget!
MyLib.WidgetOptions
MyLib.WidgetOptions.WidgetOptions() -> void
MyLib.WidgetOptions.MaxRetries.get -> int
MyLib.WidgetOptions.MaxRetries.set -> void
主な書式ルール:
!サフィックスは、nullable が有効なコンテキストでの null 非許容参照型を示します?サフィックスは、null 許容参照型または null 許容値型を示します- コンストラクターには型名を使用します(例:
Widget.Widget() -> void) - プロパティは
.getと.setのエントリに展開されます - 既定のパラメーター値はシグネチャに含まれます
Shipped/Unshipped のライフサイクル
リリースサイクル全体でのワークフロー:
開発中(リリースの合間):
- ソースコードに新しいパブリック API メンバーを追加します
- RS0016 が発生します -- メンバーが追跡されていません
- コード修正を使用するか、手動で
PublicAPI.Unshipped.txtに追加します - RS0016 が解消されます
リリース時:
PublicAPI.Unshipped.txtのすべてのエントリをPublicAPI.Shipped.txtに移動しますPublicAPI.Unshipped.txtを#nullable enableヘッダーだけの状態に戻します- リリース PR の一部として、両方のファイルをコミットします
- リリースにタグを付けます
以前リリースした API を削除する場合(メジャーバージョン):
- ソースコードからメンバーを削除します
PublicAPI.Shipped.txtからエントリを削除します- RS0017 が解消されます(発生している場合)
- リリースノートに削除内容を記載します
未リリースの API を削除する場合(リリース前):
- ソースコードからメンバーを削除します
PublicAPI.Unshipped.txtからエントリを削除します- SemVer への影響はありません -- API はリリースされていないためです
マルチ TFM プロジェクト
複数のターゲットを対象とするプロジェクトでは、PublicApiAnalyzers は、API サーフェスがターゲットごとに異なる場合に、TFM ごとの追跡ファイルをサポートします:
MyLib/
MyLib.csproj
PublicAPI.Shipped.txt # Shared across all TFMs
PublicAPI.Unshipped.txt # Shared across all TFMs
PublicAPI.Shipped.net8.0.txt # net8.0-specific APIs
PublicAPI.Unshipped.net8.0.txt # net8.0-specific APIs
PublicAPI.Shipped.net10.0.txt # net10.0-specific APIs
PublicAPI.Unshipped.net10.0.txt # net10.0-specific APIs
共通ファイルには、すべての TFM に共通する API が含まれます。TFM 固有のファイルには、そのターゲットにのみ存在する API が含まれます。アナライザーはビルド時にこれらをマージします。
TFM ごとのファイルを有効にするには、.csproj に次を追加します:
<PropertyGroup>
<RoslynPublicApiPerTfm>true</RoslynPublicApiPerTfm>
</PropertyGroup>
マルチ TFM のパッケージングの仕組みについては、[skill:dotnet-multi-targeting] を参照してください。
CI との統合
PublicApiAnalyzers は標準ビルドの一部として実行されます。CI でこれを強制するには、RS シリーズのルールについて警告をエラーとして扱うようにします:
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);RS0016;RS0017;RS0036;RS0037</WarningsAsErrors>
</PropertyGroup>
これにより、宣言されていないパブリック API の変更があると、CI ビルドが失敗します。ビルドを通すには、開発者が追跡ファイルを明示的に更新する必要があります。
API サーフェスのスナップショットパターンを検証する
Verify ライブラリを使用して、アセンブリのパブリック API サーフェス全体をスナップショットテストします。この方法ではリフレクションを使用して、すべてのパブリック型とメンバーを列挙し、人が読みやすいスナップショットを生成します。スナップショットはソース管理にコミットされ、テストの実行ごとに比較されます。パブリック API サーフェスに変更があると、スナップショットが明示的に承認されるまでテストは失敗します。
このパターンは PublicApiAnalyzers を補完します。アナライザーはプロジェクト内のビルド時に変更を検出し、Verify のスナップショットはコンパイル済みアセンブリを利用する側の視点から変更を検出します。
Verify の基本事項(セットアップ、スクラブ、コンバーター、差分ツールの統合、CI の構成)については、[skill:dotnet-snapshot-testing] を参照してください。
パブリック API サーフェスを抽出する
アセンブリをリフレクションで調べ、すべてのパブリック型とそのメンバーを安定した順序で並べた表現を生成するヘルパーメソッドを作成します:
using System.Reflection;
using System.Text;
public static class PublicApiExtractor
{
public static string GetPublicApi(Assembly assembly)
{
var sb = new StringBuilder();
var publicTypes = assembly
.GetTypes()
.Where(t => t.IsPublic || t.IsNestedPublic)
.OrderBy(t => t.FullName, StringComparer.Ordinal);
foreach (var type in publicTypes)
{
AppendType(sb, type);
}
return sb.ToString();
}
private static void AppendType(StringBuilder sb, Type type)
{
var kind = type switch
{
{ IsEnum: true } => "enum",
{ IsValueType: true } => "struct",
{ IsInterface: true } => "interface",
{ IsAbstract: true, IsSealed: true } => "static class",
{ IsAbstract: true } => "abstract class",
{ IsSealed: true } => "sealed class",
_ => "class"
};
sb.AppendLine($"{kind} {type.FullName}");
var members = type
.GetMembers(BindingFlags.Public | BindingFlags.Instance
| BindingFlags.Static | BindingFlags.DeclaredOnly)
.OrderBy(m => m.MemberType)
.ThenBy(m => m.Name, StringComparer.Ordinal)
.ThenBy(m => m.ToString(), StringComparer.Ordinal);
foreach (var member in members)
{
sb.AppendLine($" {FormatMember(member)}");
}
sb.AppendLine();
}
private static string FormatMember(MemberInfo member) =>
member switch
{
ConstructorInfo c => $".ctor({FormatParameters(c.GetParameters())})",
MethodInfo m when !m.IsSpecialName =>
$"{m.ReturnType.Name} {m.Name}({FormatParameters(m.GetParameters())})",
PropertyInfo p => $"{p.PropertyType.Name} {p.Name} {{ {GetAccessors(p)} }}",
FieldInfo f => $"{f.FieldType.Name} {f.Name}",
EventInfo e => $"event {e.EventHandlerType?.Name} {e.Name}",
_ => member.ToString() ?? string.Empty
};
private static string FormatParameters(ParameterInfo[] parameters) =>
string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"));
private static string GetAccessors(PropertyInfo prop)
{
var parts = new List<string>();
if (prop.GetMethod?.IsPublic == true) parts.Add("get;");
if (prop.SetMethod?.IsPublic == true) parts.Add("set;");
return string.Join(" ", parts);
}
}
スナップショットテストを作成する
[UsesVerify]
public class PublicApiSurfaceTests
{
[Fact]
public Task PublicApi_ShouldMatchApprovedSurface()
{
var assembly = typeof(Widget).Assembly;
var publicApi = PublicApiExtractor.GetPublicApi(assembly);
return Verify(publicApi);
}
}
初回実行時に、パブリック API の一覧全体を含む .verified.txt ファイルが作成されます。以降の実行では、現在の API サーフェスと承認済みのスナップショットが比較されます。パブリックメンバーの追加、削除、変更があると、差分が明確に示され、テストが失敗します。
API サーフェスの変更をレビューする
スナップショットテストが失敗した場合:
- Verify が、新しい API サーフェスを示す
.received.txtファイルを生成します .received.txtと.verified.txtの差分を確認して、変更内容をレビューします- 変更が意図したものであれば、
verify acceptで新しいスナップショットを受け入れます - 変更が意図しないものであれば、コードの変更を元に戻します
これにより、プルリクエストでスナップショットの差分をレビューする担当者がすべてのパブリック API 変更を明示的に承認する、コードレビューのチェックポイントが設けられます。
PublicApiAnalyzers との併用
2 つのアプローチは、それぞれ異なる目的に役立ちます。
| 懸念事項 | PublicApiAnalyzers | Verify スナップショット |
|---|---|---|
| 検出タイミング | ビルド時(IDE 内) | テスト時(コンパイル後) |
| 粒度 | メンバーごとのシグネチャ | アセンブリ全体のサーフェス |
| Nullable 注釈 | #nullable enable を通じて追跡 | 明示的なリフレクションが必要 |
| 承認ワークフロー | テキストファイル(shipped/unshipped)を編集 | スナップショットの差分を承認 |
| マルチ TFM | TFM ごとのファイル | TFM ごとのテストターゲット |
| CI ゲート | 警告をエラーとして扱う | テストの失敗 |
最大限のカバレッジを得るには、両方を使用します。PublicApiAnalyzers は開発中に変更を検出し、Verify スナップショットはテストスイートでエンドツーエンドのアセンブリレベルの検証を行います。
ApiCompat による CI での強制
ApiCompat は 2 つのアセンブリ(またはベースラインとなる NuGet パッケージと現在のビルド)を比較し、API の差異を報告します。CI に統合すると、API サーフェスの変更に応じてプルリクエストをゲートします。破壊的変更があるとビルドエラーになり、作成者は明示的に承認する必要があります。
EnablePackageValidation の基本と抑制ファイルの仕組みについては、[skill:dotnet-nuget-authoring] と [skill:dotnet-multi-targeting] を参照してください。
CI でのパッケージ検証
最も簡単な方法は、dotnet pack の実行時に EnablePackageValidation を使用することです。
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
</PropertyGroup>
CI パイプラインでは、dotnet pack の実行時にパッケージ検証が自動的に行われます。
# GitHub Actions -- gate PRs on API compatibility
name: API Compatibility Check
on:
pull_request:
paths:
- 'src/**'
- '*.props'
- '*.targets'
jobs:
api-compat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Pack with API validation
run: dotnet pack --configuration Release --no-build
# EnablePackageValidation runs during pack and fails
# the build if breaking changes are detected
アセンブリ比較用のスタンドアロン ApiCompat ツール
パッケージ化せずにアセンブリを比較する必要がある場合(たとえば、機能ブランチのビルドと main ブランチのビルドを比較する場合)は、スタンドアロンの ApiCompat ツールを使用します。
# GitHub Actions -- compare assemblies directly
name: API Diff Check
on:
pull_request:
jobs:
api-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Install ApiCompat tool
run: dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
- name: Build current branch
run: dotnet build src/MyLib/MyLib.csproj -c Release -o artifacts/current
- name: Build baseline (main branch)
run: |
git stash
git checkout origin/main -- src/MyLib/
dotnet build src/MyLib/MyLib.csproj -c Release -o artifacts/baseline
git checkout - -- src/MyLib/
git stash pop || true
- name: Compare APIs
run: |
apicompat --left-assembly artifacts/baseline/MyLib.dll \
--right-assembly artifacts/current/MyLib.dll
API 変更に対する PR のラベル付け
ApiCompat と PR のラベル付けを組み合わせると、API の変更をレビュー担当者に明示できます。
- name: Check for API changes
id: api-check
continue-on-error: true
run: |
apicompat --left-assembly artifacts/baseline/MyLib.dll \
--right-assembly artifacts/current/MyLib.dll 2>&1 | tee api-diff.txt
echo "has_changes=$([[ -s api-diff.txt ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Label PR with API changes
if: steps.api-check.outputs.has_changes == 'true'
run: gh pr edit "${{ github.event.pull_request.number }}" --add-label "api-change"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
意図的な破壊的変更の処理
破壊的変更が意図的なものである場合(新しいメジャーバージョンなど)、抑制ファイルを生成します。
dotnet pack /p:GenerateCompatibilitySuppressionFile=true
これにより、プロジェクトディレクトリに CompatibilitySuppressions.xml が作成されます。別の場所に保存する場合は、明示的に参照します。
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
注:ApiCompatSuppressionFile は ItemGroup の項目であり、PropertyGroup のプロパティではありません。PropertyGroup の構文を使用しても、何も起こりません。
抑制ファイルには、許容された特定の破壊的変更が記録されます。
<?xml version="1.0" encoding="utf-8"?>
<Suppressions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:MyLib.Widget.Calculate</Target>
<Left>lib/net8.0/MyLib.dll</Left>
<Right>lib/net8.0/MyLib.dll</Right>
</Suppression>
</Suppressions>
抑制ファイルをソース管理にコミットします。レビュー担当者はファイルを確認し、破壊的変更が記録され、意図的なものであることを検証できます。
CI での PublicApiAnalyzers ファイルの強制
PublicApiAnalyzers の警告をエラーとして扱う設定と、追跡ファイルが古くなっていないことを検証する CI のステップを組み合わせます。
- name: Build with API tracking enforcement
run: dotnet build -c Release /p:TreatWarningsAsErrors=true /warnaserror:RS0016,RS0017,RS0036,RS0037
- name: Verify PublicAPI files are committed
run: |
if git diff --name-only | grep -q 'PublicAPI'; then
echo "::error::PublicAPI tracking files have uncommitted changes"
git diff -- '**/PublicAPI.*.txt'
exit 1
fi
複数ライブラリのモノレポでの強制
複数のライブラリを含むリポジトリでは、ソリューションレベルで API 検証を適用します。
<Project>
<PropertyGroup Condition="'$(IsPackable)' == 'true'">
<EnablePackageValidation>true</EnablePackageValidation>
<WarningsAsErrors>$(WarningsAsErrors);RS0016;RS0017;RS0036;RS0037</WarningsAsErrors>
</PropertyGroup>
<ItemGroup Condition="'$(IsPackable)' == 'true'">
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers"
Version="3.3.*" PrivateAssets="all" />
</ItemGroup>
</Project>
これにより、構成を重複させることなく、リポジトリ内のパッケージ化可能なすべてのプロジェクトで PublicApiAnalyzers とパッケージ検証の両方が有効になります。
エージェントが陥りやすい問題
PublicAPI.Shipped.txtとPublicAPI.Unshipped.txtの両方を作成し忘れないでください -- PublicApiAnalyzers では、空であっても両方のファイルが存在する必要があります。ファイルがないと、すべてのパブリックメンバーで RS0037 警告が発生します。- PublicAPI の追跡ファイルから
#nullable enableヘッダーを省略しないでください -- ヘッダーがない場合(RS0036)、アナライザーは nullable 注釈の差分を無視するため、nullable が有効なライブラリで実際に発生した API サーフェスの変更を見落とします。 ApiCompatSuppressionFileを PropertyGroup に配置しないでください -- これは ItemGroup の項目です(<ApiCompatSuppressionFile Include="..." />)。PropertyGroup の構文は警告なしに無視され、抑制は機能しません。- 開発途中で
PublicAPI.Unshipped.txtの項目をPublicAPI.Shipped.txtに移動しないでください -- 項目を移動するのはリリース時だけにしてください。早まって出荷済みにすると、未リリースの API 追加をきれいに取り消せなくなります。 - Verify API サーフェススナップショットだけを検証手段として使わないでください -- これはコンパイル後のテスト実行時に動作します。ビルド時にすぐフィードバックを得るには PublicApiAnalyzers を、ベースラインとの比較には ApiCompat を使用し、Verify スナップショットは追加のセーフティネットとして導入してください。
- CI の ApiCompat ワークフローで TFM 固有のパスをハードコードしないでください -- TFM の追加や変更による問題を避けるため、MSBuild の出力パス変数を使用するか、TFM をパラメーター化してください。
<NoWarn>で RS0016 をグローバルに抑制しないでください -- これにより、すべてのパブリック API の追跡が気づかれないまま無効になります。代わりに、不足している API 項目を追跡ファイルに追加してください。API を意図的に内部用としながらもpublicにする必要がある場合(たとえば、InternalsVisibleToの代替手段のため)は、[EditorBrowsable(EditorBrowsableState.Never)]を使用し、追跡ファイルに追加してください。GenerateCompatibilitySuppressionFile=trueで抑制ファイルを生成した後、確認を忘れないでください -- 意図した範囲を超えて変更が抑制される可能性があります。コミットする前に、生成された XML を必ず確認してください。
前提条件
- .NET 8.0+ SDK
Microsoft.CodeAnalysis.PublicApiAnalyzersNuGet パッケージ(RS0016/RS0017 診断用)EnablePackageValidationMSBuild プロパティ(dotnet pack中のベースライン API 比較用)Microsoft.DotNet.ApiCompat.Tool(任意。dotnet packの外でアセンブリを単独比較する場合に使用)- Verify テストライブラリとテストフレームワーク統合パッケージ(API サーフェスのスナップショットテスト用) -- セットアップについては [スキル:dotnet-snapshot-testing] を参照してください
- バイナリ互換性とソース互換性のルールに関する理解 -- [スキル:dotnet-library-api-compat] を参照してください