$npx -y skills add wshaddix/dotnet-skills --skill dotnet-api-surface-validationDetecting API changes in CI. PublicApiAnalyzers, Verify snapshots, breaking change enforcement.
| 1 | # dotnet-api-surface-validation |
| 2 | |
| 3 | Tools and workflows for validating and tracking the public API surface of .NET libraries. Covers three complementary approaches: **PublicApiAnalyzers** for text-file tracking of shipped/unshipped APIs with Roslyn diagnostics, the **Verify snapshot pattern** for reflection-based API surface snapshot testing, and **ApiCompat CI enforcement** for gating pull requests on API surface changes. |
| 4 | |
| 5 | **Version assumptions:** .NET 8.0+ baseline. PublicApiAnalyzers 3.3+ (ships with `Microsoft.CodeAnalysis.Analyzers` or standalone `Microsoft.CodeAnalysis.PublicApiAnalyzers`). ApiCompat tooling included in .NET 8+ SDK. |
| 6 | |
| 7 | **Out of scope:** Binary vs source compatibility rules, type forwarders, SemVer impact -- see [skill:dotnet-library-api-compat]. NuGet packaging, `EnablePackageValidation` basics, and suppression file mechanics -- see [skill:dotnet-nuget-authoring] and [skill:dotnet-multi-targeting]. Verify library fundamentals (setup, scrubbing, converters) -- see [skill:dotnet-snapshot-testing]. General Roslyn analyzer configuration (EditorConfig, severity levels) -- see [skill:dotnet-roslyn-analyzers]. HTTP API versioning -- see [skill:dotnet-api-versioning]. |
| 8 | |
| 9 | Cross-references: [skill:dotnet-library-api-compat] for binary/source compatibility rules, [skill:dotnet-nuget-authoring] for `EnablePackageValidation` and NuGet SemVer, [skill:dotnet-multi-targeting] for multi-TFM ApiCompat tool mechanics, [skill:dotnet-snapshot-testing] for Verify fundamentals, [skill:dotnet-roslyn-analyzers] for general analyzer configuration, [skill:dotnet-api-versioning] for HTTP API versioning. |
| 10 | |
| 11 | --- |
| 12 | |
| 13 | ## PublicApiAnalyzers |
| 14 | |
| 15 | PublicApiAnalyzers tracks every public API member in text files committed to source control. The analyzer enforces that new APIs go through an explicit "unshipped" phase before being marked "shipped," preventing accidental public API exposure and undocumented surface area changes. |
| 16 | |
| 17 | ### Setup |
| 18 | |
| 19 | Install the analyzer package: |
| 20 | |
| 21 | ```xml |
| 22 | <ItemGroup> |
| 23 | <PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.*" PrivateAssets="all" /> |
| 24 | </ItemGroup> |
| 25 | ``` |
| 26 | |
| 27 | Create the two tracking files at the project root (adjacent to the `.csproj`): |
| 28 | |
| 29 | ``` |
| 30 | MyLib/ |
| 31 | MyLib.csproj |
| 32 | PublicAPI.Shipped.txt # APIs shipped in released versions |
| 33 | PublicAPI.Unshipped.txt # APIs added since last release |
| 34 | ``` |
| 35 | |
| 36 | Both files must exist, even if empty. Each must contain a header comment: |
| 37 | |
| 38 | ``` |
| 39 | #nullable enable |
| 40 | ``` |
| 41 | |
| 42 | The `#nullable enable` header tells the analyzer to track nullable annotations in API signatures. Without it, nullable context differences are ignored. |
| 43 | |
| 44 | ### Diagnostic Rules |
| 45 | |
| 46 | | Rule | Severity | Meaning | |
| 47 | |------|----------|---------| |
| 48 | | RS0016 | Warning | Public API member not declared in API tracking files | |
| 49 | | RS0017 | Warning | Public API member removed but still in tracking files | |
| 50 | | RS0024 | Warning | Public API member has wrong nullable annotation | |
| 51 | | RS0025 | Warning | Public API symbol marked shipped but has changed signature | |
| 52 | | RS0026 | Warning | New public API added without `PublicAPI.Unshipped.txt` entry | |
| 53 | | RS0036 | Warning | API file missing `#nullable enable` header | |
| 54 | | RS0037 | Warning | Public API declared but does not exist in source | |
| 55 | |
| 56 | **RS0016** is the most common diagnostic. When you add a new `public` or `protected` member, RS0016 fires until you add the member's signature to `PublicAPI.Unshipped.txt`. Use the code fix (lightbulb) in the IDE to automatically add the entry. |
| 57 | |
| 58 | **RS0017** fires when you remove or rename a `public` member but the old signature still exists in the tracking files. Remove the stale line from the appropriate file. |
| 59 | |
| 60 | ### File Format |
| 61 | |
| 62 | Each line in the tracking files represents one public API symbol using its documentation comment ID format: |
| 63 | |
| 64 | ``` |
| 65 | #nullable enable |
| 66 | MyLib.Widget |
| 67 | MyLib.Widget.Widget() -> void |
| 68 | MyLib.Widget.Name.get -> string! |
| 69 | MyLib.Widget.Name.set -> void |
| 70 | MyLib.Widget.Calculate(int count) -> decimal |
| 71 | MyLib.Widget.CalculateAsync(int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<decimal>! |
| 72 | MyLib.IWidgetFactory |
| 73 | MyLib.IWidgetFactory.Create(string! name) -> MyLib.Widget! |
| 74 | MyLib.WidgetOptions |
| 75 | MyLib.WidgetOptions.WidgetOptions() -> void |
| 76 | MyLib.WidgetOptions.MaxRetries.get -> int |
| 77 | MyLib.WidgetOptions.MaxRetries.set -> void |
| 78 | ``` |
| 79 | |
| 80 | Key formatting rules: |
| 81 | - The `!` suffix denotes a non-nullable reference type in nullable-enabled context |
| 82 | - The `?` suffix denotes a nullable reference type or nullable value type |
| 83 | - Constructors use the type name (e.g., `Widget.Widget() -> void`) |
| 84 | - Properties expand to `.get` and `.set` entries |
| 85 | - Default parameter values are included in the signature |
| 86 | |
| 87 | ### Shipped/Unshipped Lifecycle |
| 88 | |
| 89 | The workflow across release cycles: |
| 90 | |
| 91 | **During development (between releases):** |
| 92 | |
| 93 | 1. Add new public API member to |