| title | NuGet package compatibility rules |
|---|---|
| description | Learn about the compatibility rules that NuGet packages must follow to ensure consumers can reliably update and use packages across .NET frameworks and versions. |
| ms.date | 03/31/2026 |
NuGet packages are the primary distribution mechanism for .NET libraries. To ensure that consumers can reliably update packages and use them across frameworks and versions, package authors must follow a set of compatibility rules. These rules ensure that newer versions of a package can replace older versions without breaking applications at build time or run time.
This article describes the compatibility rules that apply to NuGet packages, with specific attention to framework compatibility, assembly compatibility, dependency compatibility, and assembly versioning.
NuGet packages can target multiple frameworks by including assemblies for different target framework monikers (TFMs). When a package supports multiple frameworks, there is an implied compatibility relationship between those frameworks. Specifically, newer or more specific frameworks are compatible with older or more general ones. For example:
net8.0is compatible withnetstandard2.0net462(.NET Framework 4.6.2) is compatible withnetstandard2.0net9.0is compatible withnet8.0
When a consumer's project targets a framework that isn't directly included in a package, NuGet selects the best matching compatible framework from the package. For example, a net8.0 application consuming a package that contains only a netstandard2.0 assembly will use that netstandard2.0 assembly. If the package also contains a net8.0 assembly, that more specific assembly is used instead.
Because NuGet can select different assemblies for different consumers, the assemblies provided for compatible frameworks must themselves be compatible. The netstandard2.0 assembly and the net8.0 assembly in the same package must expose the same (or a superset of) API, and must not differ in behavior in breaking ways.
For more information about framework compatibility, see .NET Standard and Target frameworks in SDK-style projects.
The Package Validation feature in the .NET SDK automatically validates that assemblies for compatible frameworks within a package are consistent. Enable it by adding the following property to your project file:
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
</PropertyGroup>The compatible framework in package validator checks, for example, that your netstandard2.0 assembly and net8.0 assembly don't have API inconsistencies.
These rules apply whenever one assembly must be compatible with another—whether it's a newer version of the same assembly replacing a previous version, or an assembly for a more specific framework (such as net8.0) that can be substituted for a more general one (such as netstandard2.0) within the same package.
For an assembly to be considered compatible, it must meet two requirements:
-
Equal or higher assembly version. The assembly version (
AssemblyVersionAttribute) must be greater than or equal to the previous version. Decreasing an assembly version is never an acceptable change—it breaks the runtime's ability to load the correct assembly and can causeFileLoadExceptionerrors at run time. -
No breaking API changes. The assembly must not remove or change public API in any binary-breaking way. This means no removal of public types or members, no changes to method signatures, and no other changes that would cause a
MissingMethodException,MissingMemberException, orTypeLoadExceptionat run time.
Decreasing an assembly version is never acceptable, even across major package versions. Assembly versions are used by the runtime to resolve which assembly to load. When a library is compiled against assembly version 2.0.0.0, the runtime requires that version or higher to be present. If a newer package ships with a lower assembly version, any library that was compiled against the higher version fails to load.
While it's technically possible to work around this with a binding redirect or an AssemblyResolve handler to redirect the higher version to the lower one, this is always a breaking change in practice. The higher assembly version exists because it shipped with newer API and bug fixes that consumers have come to depend on. Redirecting to an older assembly means those APIs and fixes are no longer present, leading to MissingMethodException or incorrect behavior at run time.
Binary breaking changes—changes that cause previously compiled code to fail at run time—may be permissible across major package versions, but authors should understand the ecosystem impact. Any library in the ecosystem that depends on your package also exposes your API surface transitively. A binary breaking change in your package can break consumers of those downstream libraries, even if they don't directly reference your package.
The .NET libraries maintain a very high bar for binary compatibility. For a detailed list of what constitutes a breaking change, see the breaking change rules in the dotnet/runtime repository.
Tip
Use Package Validation with a baseline version to automatically detect binary breaking changes between releases:
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>You can also use the Microsoft.DotNet.ApiCompat.Tool to compare assemblies or packages outside of the build process.
The dependencies declared by a NuGet package are part of its public contract. When a consumer installs your package, they also get all of its dependencies. Dropping a dependency in a newer version of your package can break consumers who rely on types from that dependency being present at run time.
Packages should avoid removing dependencies in compatible framework versions or within the same major version series. Dropping a dependency can cause run-time failures for consumers who rely on types from that dependency, even if your own code no longer uses it directly.
Dropping a dependency is more reasonable across major version boundaries. Unlike binary breaking changes, a dropped dependency can be mitigated by the consuming application without recompiling intermediate libraries—the application can simply add a direct reference to the removed dependency.
One place where the .NET libraries intentionally drop dependencies is for polyfill packages such as Microsoft.Bcl.AsyncInterfaces, Microsoft.Bcl.HashCode, and Microsoft.Bcl.Memory. These packages provide functionality on older frameworks (such as .NET Standard 2.0 and .NET Framework) that is built into newer .NET versions.
For packages that target multiple frameworks, it's appropriate to include the polyfill dependency only for older TFMs and drop it for the TFMs where the functionality is provided by the framework itself. For example:
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
</ItemGroup>
<!-- No dependency needed for net8.0, where IAsyncEnumerable is built-in -->We recommend that library authors consuming polyfill packages follow the same pattern to avoid unnecessary assemblies in applications targeting the latest frameworks.
Note
When a polyfill dependency is dropped for a newer TFM, make sure that the package includes a TFM-specific assembly for that framework. Otherwise, consumers on the newer framework may resolve the netstandard2.0 assembly, which still expects the polyfill at run time. Multi-targeting with explicit TFM-specific assets avoids this problem.
Assembly versions are distinct from NuGet package versions. While package versions follow Semantic Versioning (SemVer) conventions, assembly versions are four-part version numbers (Major.Minor.Build.Revision) used by the .NET runtime to resolve and load assemblies.
The assembly version is embedded in compiled references. When library A is compiled against version 2.0.0.0 of library B, the runtime requires that library B's assembly version be 2.0.0.0 or higher at run time. This binding behavior is what makes assembly version changes important for package compatibility.
On .NET Framework, assembly versioning has additional implications due to the runtime's loading behavior:
-
Global Assembly Cache (GAC) preference. The .NET Framework loader always prefers an assembly from the GAC over an app-local copy. Packages that do not change their assembly version across releases cannot guarantee that serviced (patched) versions will be loaded on .NET Framework, because the GAC may contain an older copy with the same assembly version.
-
Binding redirects are required. The .NET Framework loader requires binding redirects to unify different assembly versions. When an application consumes multiple packages that depend on different versions of a shared library, binding redirects tell the runtime to load a single (higher) version for all callers.
Applications consuming NuGet packages on .NET Framework must account for binding redirects to unify the versions of transitive dependencies. It is never safe to assume that binding redirects won't be required—doing so makes an application unable to update a mid-stack library in the event of a critical security update.
Tip
Enable <AutoGenerateBindingRedirects> in your application project to have the build system automatically generate the necessary binding redirects.
For most packages, the assembly version changes with each release. This is essential to ensure loading of patched binaries, particularly on .NET Framework where the GAC and binding redirect behavior require version changes for correct assembly resolution.
Some .NET packages ship assemblies that also exist in .NET shared frameworks. Examples include packages like System.Text.Json and System.Collections.Immutable, among others. The specific set of packages that overlap with a given shared framework varies by release and is not a fixed or documented list. These packages are unique: they provide API that is also available in .NET without a package reference, but they allow applications targeting older frameworks to use newer API.
To support these overlapping packages, the .NET SDK and runtime include several features:
-
Conflict resolution. Conflict resolution occurs at two levels, both following the same principle: prefer the newer version, and prefer the framework's copy when versions are equal.
- At build time, the SDK's
ResolvePackageFileConflictstask compares assemblies from packages against those provided by the shared framework and selects the winner for the application's output. - At run time, the .NET host performs the same logic when probing for assemblies, as described in the assembly conflict resolution design document.
- At build time, the SDK's
-
NuGet package pruning. The SDK can prune packages from the dependency graph when the shared framework already provides the same functionality. This was introduced as an opt-in feature in the .NET 9 SDK and is enabled by default in the .NET 10 SDK for projects targeting .NET 10. Package pruning reduces restore time, shrinks dependency graphs, and eliminates false positives from vulnerability scanners like NuGet Audit. For more information, see NuGet Warning NU1510.
These features allow packages to be used when needed on older frameworks and transparently replaced by the shared framework on newer ones.
Packages that overlap with .NET shared frameworks follow a special assembly versioning policy:
-
The assembly version is held constant at
Major.Minor.0.0for a given major/minor release. For example, all 8.0.x versions ofSystem.Text.Jsonship with assembly version8.0.0.0. This ensures that the package's assembly can be transparently replaced by the shared framework's copy of the same major/minor version. -
.NET Standard assemblies in these packages follow the same rule, since they may run on any compatible framework (including .NET itself).
-
.NET Framework assemblies in these packages are the exception—they do increment their assembly version with each servicing release. This is necessary because .NET Framework's GAC and binding redirect behavior requires version changes to guarantee that patched assemblies are loaded. As a result, the .NET Framework assemblies in these packages are the only ones whose assembly version changes across servicing releases.
This policy ensures that:
- Applications targeting modern .NET get the shared framework's copy of the assembly seamlessly.
- Applications targeting .NET Framework get properly serviced assemblies with correct binding behavior.
- Libraries targeting .NET Standard work correctly on both platforms.
Packages that do not overlap with a shared framework are free to increment their assembly version with every release. This is the typical and recommended pattern, as it ensures that patched binaries are always loaded correctly regardless of the target framework.
| Rule | Requirement |
|---|---|
| Compatible frameworks must provide compatible assemblies | Assemblies for more specific TFMs must be API-compatible with assemblies for more general TFMs in the same package. |
| Assembly versions must not decrease | Never ship a newer package version with a lower assembly version than a previous release. |
| API must not break in compatible versions | Don't remove or change public API in binary-breaking ways within compatible versions. Major versions have more latitude. |
| Dependencies should not be dropped in compatible versions | Removing a package dependency can break consumers. Dropping dependencies is more acceptable across major versions. |
| Polyfill dependencies should be dropped for inbox frameworks | When a polyfill (Microsoft.BCL.*) provides functionality that is built into the target framework, the dependency should be omitted for that TFM. |
| Shared framework packages hold assembly version at Major.Minor.0.0 | Packages overlapping with .NET shared frameworks keep assembly version constant within a release series, except for .NET Framework TFMs. |
- Versioning and .NET libraries
- Breaking changes and .NET libraries
- Dependencies and .NET libraries
- .NET Package Validation overview
- Microsoft.DotNet.ApiCompat.Tool
- .NET Standard
- Target frameworks in SDK-style projects
- Breaking change rules (dotnet/runtime)
- Assembly conflict resolution design
- Redirecting assembly versions (.NET Framework)