Package Validation and API Compatibility
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
A binary breaking change compiles fine in the library and fails at run time in someone else’s application. The SDK can
catch most of them at pack time: package validation (since the .NET 6 SDK) compares the new package with itself
across target frameworks and runtimes, and with a previously published baseline version, using the APICompat
engine. This page turns it on for Ledgerline, breaks the API on purpose, and reads the errors, with .NET SDK
10.0.402. Which changes are breaking is explained on
Designing a Library for NuGet.
Turning it on
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<!-- compare with the last published release -->
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>
The validation runs as part of Pack. Three validators:
-
Compatible framework in package — code compiled against one framework of the package must run against every other compatible one (
netstandard2.0vsnet8.0vsnet10.0inLedgerline). Always on. -
Compatible runtime — RID-specific implementation assemblies (
runtimes/<rid>/lib/) must match each other and the compile-time assembly. Always on. -
Baseline version — the package must be compatible with the baseline package, which restore downloads from the configured feeds like any other package. On only when
PackageValidationBaselineVersionis set.
A removed member: CP0002
Ledgerline 1.0.0 is in the local feed. Version 1.1.0 removes LedgerReader.ReadFile(string):
dotnet pack src/Ledgerline -o artifacts/pv # Version 1.1.0, baseline 1.0.0
error : API compatibility errors between 'lib/net10.0/Ledgerline.dll' (.../nuget-packages/ledgerline/1.0.0/ledgerline.1.0.0.nupkg) and 'lib/net10.0/Ledgerline.dll' (artifacts/pv/Ledgerline.1.1.0.nupkg):
error CP0002: Member 'System.Collections.Generic.IReadOnlyList<Ledgerline.JournalLine> Ledgerline.LedgerReader.ReadFile(string)' exists on [Baseline] lib/net10.0/Ledgerline.dll but not on lib/net10.0/Ledgerline.dll
error CP0002: Member '...ReadFile(string)' exists on [Baseline] lib/net8.0/Ledgerline.dll but not on lib/net8.0/Ledgerline.dll
error CP0002: Member '...ReadFile(string)' exists on [Baseline] lib/netstandard2.0/Ledgerline.dll but not on lib/netstandard2.0/Ledgerline.dll
error : API breaking changes found. If those are intentional, the APICompat suppression file can be updated by rebuilding with '/p:ApiCompatGenerateSuppressionFile=true'
(The messages from Microsoft.NET.ApiCompat.ValidatePackage.targets are shortened.) One error per target framework.
The build fails — but the .nupkg had already been written to artifacts/pv (25,383 bytes), so a script must check
the exit code, not the presence of the file.
Source: Baseline package validator.
An API that differs between frameworks: CP0001
A public type compiled only for netstandard2.0 (#if !NET8_0_OR_GREATER) breaks the "compatible frameworks" rule — a net8.0 application compiled against the netstandard2.0 assembly by another library would fail to find it:
error CP0001: Type 'Ledgerline.LegacyHelpers' exists on lib/netstandard2.0/Ledgerline.dll but not on lib/net8.0/Ledgerline.dll
The opposite (an API only on the newer framework) is allowed by default and reported in strict mode.
Strict mode and dropped frameworks
By default only removals are errors: adding API is compatible. Strict mode requires the two sides to be identical — useful for a servicing (patch) release that must not add API. A 1.1.0 with a new LedgerStats class, packed with
-p:EnableStrictModeForBaselineValidation=true:
error CP0001: Type 'Ledgerline.LedgerStats' exists on lib/net10.0/Ledgerline.dll but not on [Baseline] lib/net10.0/Ledgerline.dll
error CP0003: lib/net10.0/Ledgerline.dll assembly version '1.1.0.0' should be equal to [Baseline] lib/net10.0/Ledgerline.dll version '1.0.0.0'.
(and the same for net8.0 and netstandard2.0). Strict mode also requires the same assembly version — so it fits
patch releases with a fixed AssemblyVersion, not a minor release. Without strict mode the same package validated
cleanly. The three switches: EnableStrictModeForBaselineValidation, EnableStrictModeForCompatibleFrameworksInPackage
and EnableStrictModeForCompatibleTfms.
Dropping a target framework is a breaking change too:
error PKV006: Target framework .NETStandard,Version=v2.0 is no longer supported in the latest version.
Other properties: PackageValidationBaselineName (a baseline with a different ID, after a rename),
PackageValidationBaselineFrameworkToIgnore items (frameworks of the baseline deliberately dropped) and
PackageValidationReferencePath items (reference assemblies per framework, when the validator can’t resolve
dependencies).
Source: Compatible framework validator: Strict mode, Compatible framework in package: Strict mode, API compatibility: Strict mode, Package validation properties, EnableStrictModeForBaselineValidation, EnableStrictModeForCompatibleFrameworksInPackage, PackageValidationBaselineFrameworkToIgnore, PackageValidationBaselineName, PackageValidationBaselineVersion, PackageValidationReferencePath.
Suppressing an intentional break
When a break is intended (a major version), record it instead of disabling validation:
dotnet pack src/Ledgerline -o artifacts/pv -p:ApiCompatGenerateSuppressionFile=true
Successfully wrote compatibility suppressions to 'src/Ledgerline/CompatibilitySuppressions.xml'.
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Ledgerline.LedgerReader.ReadFile(System.String)</Target>
<Left>lib/net10.0/Ledgerline.dll</Left>
<Right>lib/net10.0/Ledgerline.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<!-- the same for net8.0 and netstandard2.0 -->
</Suppressions>
The next pack read the file automatically and reported no error. Commit it, so the break is reviewed like code. The
error message names ApiCompatGenerateSuppressionFile; the documentation names GenerateCompatibilitySuppressionFile — both wrote the file on 10.0.402. After the release, move the baseline to the new version and delete the
suppressions.
Source: Suppress compatibility warnings.
Diagnostic IDs
| ID | Meaning |
|---|---|
CP0001 |
A public type is missing on one side. |
CP0002 |
A public member is missing on one side. |
CP0003 |
Part of the assembly identity differs (name, public key token, culture, version). |
CP0004 |
No matching assembly on one side. |
CP0005, CP0006 |
An abstract member added to an unsealed type; an interface member added without a default implementation. |
CP0007, CP0008 |
A base type or base interface removed. |
CP0009 |
A type became sealed. |
CP0010, CP0011 |
An enum’s underlying type or a member’s value changed. |
CP0012, CP0013 |
|
CP0014 — CP0016 |
Attributes removed, changed or added (only with attribute checks on). |
CP0017 |
A parameter was renamed (a source break for named arguments). |
CP0018 |
An interface member became sealed. |
CP0019, CP0020 |
Visibility reduced or expanded. |
CP1001 — CP1003 |
An assembly or reference assembly could not be found. |
PKV001 — PKV005 |
A compile-time or run-time asset is missing for a compatible framework or RID. |
PKV006, PKV007 |
A target framework, or framework and RID pair, was dropped. |
The list currently ends at CP0020; there is no CP0021. A strong-named net8.0 build next to unsigned builds of the
other frameworks gave
CP0003: lib/net8.0/Ledgerline.dll assembly public key token '719c79c5e8e6e9af' does not match with lib/net10.0/Ledgerline.dll 'null'.
Source: API compatibility diagnostic IDs.
ApiCompat outside the build
Microsoft.DotNet.ApiCompat.Tool (since the .NET 8 SDK; 10.0.402 here) compares two assemblies, or two packages,
from the command line — for example a library that isn’t a NuGet package:
dotnet tool install Microsoft.DotNet.ApiCompat.Tool --version 10.0.402 --tool-path ./apitool
./apitool/apicompat --left old/lib/net8.0/Ledgerline.dll --right src/Ledgerline/bin/Release/net8.0/Ledgerline.dll
API compatibility errors between 'old/lib/net8.0/Ledgerline.dll' (left) and '.../bin/Release/net8.0/Ledgerline.dll' (right):
CP0002: Member 'System.Collections.Generic.IReadOnlyList<Ledgerline.JournalLine> Ledgerline.LedgerReader.ReadFile(string)' exists on old/lib/net8.0/Ledgerline.dll but not on .../bin/Release/net8.0/Ledgerline.dll
API breaking changes found. If those are intentional, the APICompat suppression file can be updated by specifying the '--generate-suppression-file' parameter.
It exited with code 1. apicompat package <nupkg> --baseline-package <nupkg> runs the package validators the same way,
and inside a build ApiCompatValidateAssemblies=true (with a reference to the separate Microsoft.DotNet.ApiCompat.Task
package) compares an assembly with a contract assembly.
The documentation’s samples are old: the baseline validator page shows a net6.0 project and output from SDK
6.0.413, the framework validator pages netstandard2.0;net6.0 with output from a 6.0.100-rc.1 SDK. The messages
above are what 10.0.402 prints; the targets file named in them is now Microsoft.NET.ApiCompat.ValidatePackage.targets
instead of Microsoft.NET.Compatibility.Common.targets.
Common mistakes
-
Enabling validation without a baseline — only the cross-framework checks run; removals between releases pass.
-
A baseline the feed can’t provide — restore fails; the baseline must be on a configured source.
-
Suppressing instead of fixing — a suppression file is a list of breaks you ship.
-
Strict mode on a minor release — it rejects every added API and the new assembly version.
-
Relying on the
.nupkgexisting — it is written even when validation fails.