Skip to content

Commit 8df0261

Browse files
committed
Merge main
2 parents b41069b + f880e40 commit 8df0261

167 files changed

Lines changed: 4320 additions & 1868 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,5 @@
1-
###############################################################################
2-
# Set default behavior to automatically normalize line endings.
3-
###############################################################################
1+
# Normalize line endings
42
* text=auto
53

6-
###############################################################################
7-
# Set default behavior for command prompt diff.
8-
#
9-
# This is need for earlier builds of msysgit that does not have it on by
10-
# default for csharp files.
11-
# Note: This is only used by command line
12-
###############################################################################
13-
#*.cs diff=csharp
14-
15-
###############################################################################
16-
# Set the merge driver for project and solution files
17-
#
18-
# Merging from the command prompt will add diff markers to the files if there
19-
# are conflicts (Merging from VS is not affected by the settings below, in VS
20-
# the diff markers are never inserted). Diff markers may cause the following
21-
# file extensions to fail to load in VS. An alternative would be to treat
22-
# these files as binary and thus will always conflict and require user
23-
# intervention with every merge. To do so, just uncomment the entries below
24-
###############################################################################
25-
#*.sln merge=binary
26-
#*.csproj merge=binary
27-
#*.vbproj merge=binary
28-
#*.vcxproj merge=binary
29-
#*.vcproj merge=binary
30-
#*.dbproj merge=binary
31-
#*.fsproj merge=binary
32-
#*.lsproj merge=binary
33-
#*.wixproj merge=binary
34-
#*.modelproj merge=binary
35-
#*.sqlproj merge=binary
36-
#*.wwaproj merge=binary
37-
38-
###############################################################################
39-
# behavior for image files
40-
#
41-
# image files are treated as binary by default.
42-
###############################################################################
43-
#*.jpg binary
44-
#*.png binary
45-
#*.gif binary
46-
47-
###############################################################################
48-
# diff behavior for common document formats
49-
#
50-
# Convert binary document formats to text before diffing them. This feature
51-
# is only available from the command line. Turn it on by uncommenting the
52-
# entries below.
53-
###############################################################################
54-
#*.doc diff=astextplain
55-
#*.DOC diff=astextplain
56-
#*.docx diff=astextplain
57-
#*.DOCX diff=astextplain
58-
#*.dot diff=astextplain
59-
#*.DOT diff=astextplain
60-
#*.pdf diff=astextplain
61-
#*.PDF diff=astextplain
62-
#*.rtf diff=astextplain
63-
#*.RTF diff=astextplain
64-
65-
.github/workflows/*.lock.yml linguist-generated=true merge=ours
4+
# Treat workflow lock files as generated
5+
.github/workflows/*.lock.yml linguist-generated=true merge=ours
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
---
2+
applyTo: "**/Directory.Packages.props,**/*.csproj,**/Directory.Build.props,**/*.nuspec"
3+
---
4+
# Choosing Package Versions in Multi-Targeted Projects
5+
6+
Guidance for choosing NuGet package versions in multi-targeted projects (e.g. `net462;net8.0;net9.0`).
7+
8+
## Rule
9+
For runtime-aligned packages, **the package major must match the target runtime major**: 8.x on `net8.0`, 9.x on `net9.0`, 10.x on `net10.0`, and so on. TFMs that aren't tied to a specific runtime major (`net462`, `netstandard2.0`) get the major of the floor LTS. Other categories are versioned as described below.
10+
11+
Split package references into three categories:
12+
13+
### 1. Runtime-aligned packages — **version per TFM, matching the runtime band**
14+
15+
Packages whose major version ships with (or is tightly coupled to) a specific .NET runtime:
16+
17+
- `Microsoft.Extensions.*` (Logging, DependencyInjection, Configuration, Hosting, Options, Caching, Http, Primitives, ...)
18+
- `Microsoft.AspNetCore.*`
19+
- `Microsoft.EntityFrameworkCore.*`
20+
- `System.Text.Json`, `System.Memory`, `System.IO.Pipelines`, `System.Formats.Asn1`, `System.Security.Cryptography.Pkcs`
21+
- `Microsoft.Bcl.*`
22+
23+
Use the major version that matches the TFM. For TFMs without a corresponding runtime major (`net462`, `netstandard2.0`, etc.), use the major of the **lowest supported modern TFM** — typically the floor LTS (e.g. `8.x` while net8 is supported). This keeps the legacy targets on a long-lived, well-patched band and avoids dragging in transitive deps from a newer major:
24+
25+
```xml
26+
<!-- Defaults: lowest supported runtime band (e.g. net8 LTS); also applies to net462 / netstandard2.0 -->
27+
<ItemGroup>
28+
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
29+
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
30+
</ItemGroup>
31+
32+
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
33+
<PackageVersion Update="Microsoft.Extensions.Logging" Version="9.0.0" />
34+
<PackageVersion Update="System.Text.Json" Version="9.0.0" />
35+
</ItemGroup>
36+
```
37+
38+
When the floor LTS drops out of support, bump the default block to the new floor LTS major and drop any conditional block that becomes redundant.
39+
40+
### 2. Independent packages — **single version across all TFMs**
41+
42+
Packages whose versioning is decoupled from the .NET runtime:
43+
44+
- `Newtonsoft.Json`, `Polly.*`, `Serilog.*`
45+
- `Azure.*`, `Microsoft.Identity.*`, `Microsoft.IdentityModel.*`
46+
- `Microsoft.Data.SqlClient`, `Dapper`, `StackExchange.Redis`
47+
- Most third-party packages
48+
49+
Reference one (latest stable) version unconditionally:
50+
51+
```xml
52+
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
53+
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
54+
```
55+
56+
### 3. Polyfills — **conditional presence, single version**
57+
58+
Packages that only exist (or are only needed) on older TFMs. The polyfill major doesn't have to match any runtime band (older TFMs have no in-box equivalent), so pick the latest stable available:
59+
60+
```xml
61+
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'net462'">
62+
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
63+
</ItemGroup>
64+
```
65+
66+
Condition the *presence* of the reference, not the version.
67+
68+
## How to categorize a package
69+
70+
The named lists above aren't exhaustive. To classify a package you don't recognize, work through these steps in order:
71+
72+
### 1. Read the nuget.org description
73+
74+
Pure polyfills almost always say so explicitly. For example, `Microsoft.Bcl.TimeProvider`'s page reads: *"For apps targeting .NET 8 and newer versions, referencing this package is unnecessary, as the types it contains are already included in the .NET 8 and higher platform versions."*
75+
76+
If the description says "for apps targeting .NET X and earlier" or "unnecessary on .NET X+", treat as polyfill candidate and continue to step 2 to confirm. If it makes no such claim and the package owner is Microsoft + a major number tracks the .NET release train, treat as runtime-aligned candidate.
77+
78+
### 2. Inspect the package's `lib/` layout
79+
80+
Look at the "Frameworks" tab on nuget.org or open the `.nupkg`:
81+
82+
| `lib/` layout | Category |
83+
|---|---|
84+
| Only older TFM folders (`netstandard2.0`, `net462`) — no modern TFMs | Pure polyfill |
85+
| `lib/net8.0/_._`, `lib/net9.0/_._` placeholders + real DLL only on older TFMs | Pure polyfill (no-op on modern TFMs) |
86+
| Real DLLs in `lib/net8.0/`, `lib/net9.0/`, `lib/net10.0/`, differing per band | Runtime-aligned |
87+
| Real DLLs on every TFM including older ones, single major doesn't track .NET releases | Independent |
88+
89+
### 3. Check the release cadence
90+
91+
- Runtime-aligned: new major every November in lockstep with .NET (8.0, 9.0, 10.0, ...) plus monthly servicing patches.
92+
- Independent: releases on its own schedule, major doesn't correlate with .NET versions.
93+
- Pure polyfill: usually freezes at one major and rarely bumps; new majors only to ride the build train.
94+
95+
### 4. Functional test — remove the reference and rebuild
96+
97+
The decisive test for the polyfill-vs-runtime-aligned boundary: remove the `PackageReference` on a modern TFM (e.g. net8) and build.
98+
99+
- Builds clean → package was acting as a polyfill on that TFM. Confirm category 3.
100+
- Fails with `CS0246`/`CS1061` (missing type or method) → the package contributes API the in-box BCL doesn't have. Treat as runtime-aligned (category 1), even if the description sounds polyfill-ish.
101+
102+
### Beware hybrids
103+
104+
Some packages look like polyfills but add API beyond the in-box BCL even on modern TFMs. Treat these like runtime-aligned packages.
105+
106+
### When in doubt
107+
108+
Treat as **runtime-aligned** (category 1) and reference on every TFM with per-TFM majors.
109+
110+
- Cost of mis-classifying a true polyfill this way: a redundant `_._` asset at restore. Harmless.
111+
- Cost of mis-classifying a hybrid as a pure polyfill: a compile break on the TFMs where you dropped the reference.
112+
113+
## Why
114+
115+
### Why latest minor/patch always
116+
117+
`PackageReference` versions are minimums (`[X, ∞)`), so writing a stale minor or patch buys nothing for downgrade-safety and only loses fixes:
118+
119+
- **Security**: BCL and Extensions packages ship CVE patches in minor/patch bumps. Pinning to an older patch means a customer who doesn't transitively pull a newer version stays on the vulnerable floor.
120+
- **Bug fixes**: Same logic for non-security fixes. We have no reason to anchor customers to an older `8.0.0` when `8.0.5` is available.
121+
- **NU1605 risk is small and easy to fix**: NU1605 fires on *any* downgrade, including minor/patch within the same major (e.g. our `8.0.5` transitive vs. a customer's direct `8.0.0`). In practice this is rare and trivial to resolve — the customer bumps their direct reference to a current patch. The cost of *not* tracking latest (stale security/bug fixes for every consumer who doesn't override) is larger than the cost of an occasional one-line bump in a consumer project.
122+
- **Reduces noise from automated bumps**: Dependabot/Renovate PRs disappear if we already track latest.
123+
124+
This rule applies to all three categories. The category decides the major; "latest" decides the minor and patch.
125+
126+
### NuGet `PackageReference` versions are minimums, not pins
127+
128+
`Version="X"` means `[X, ∞)`. The resolver picks the highest version requested across the graph, with one critical exception: a **direct** reference wins over a transitive one (nearest-wins).
129+
130+
### NU1605 fires when the customer's direct version is *lower* than your transitive version
131+
132+
If your library transitively requires `Microsoft.Extensions.Logging 10.0.0` and the consuming app has a direct `<PackageReference Version="8.0.0" />`, NuGet detects a downgrade and emits **NU1605**. In modern SDKs this is an **error**, not a warning — the customer's build fails.
133+
134+
The reverse (customer's direct version higher than your transitive) resolves cleanly with no warning.
135+
136+
### The asymmetry drives the rule
137+
138+
| Your transitive version | Customer's direct version | Result |
139+
|---|---|---|
140+
| 10.x | 8.x | **NU1605 error** |
141+
| 10.x | 10.x or higher | Clean |
142+
| 8.x | 8.x or higher | Clean |
143+
144+
Pinning runtime-aligned packages to the **band matching each TFM** means a net8 consumer transitively gets 8.x (no friction with their own 8.x reference), and a net10 consumer transitively gets 10.x.
145+
146+
Pinning everything to the latest major (e.g. `10.x` unconditionally) forces every net8 customer to roll their direct references forward or hit NU1605.
147+
148+
### Independent packages don't have this problem
149+
150+
`Newtonsoft.Json 13.x`, `Azure.Identity 1.17.x`, etc. aren't tied to a runtime version. Customers don't have a "matching" version in mind, and the package's own multi-targeted assets handle TFM selection internally. One version is simpler and avoids needless conditional blocks.
151+
152+
### Framework-provided assemblies win at runtime anyway
153+
154+
On .NET 8+, packages like `System.Text.Json` and `System.Memory` are part of the shared framework. Even if you reference `System.Text.Json 9.0.0`, a net8 app uses the in-box net8 copy at runtime. Referencing 10.x on net8 just creates restore-graph noise with no runtime benefit — another reason to match the band.
155+
156+
## Tradeoffs and alternatives considered
157+
158+
### Per-TFM (the rule) vs. single lowest-LTS version
159+
160+
A "pin everything to the lowest supported LTS major (e.g. 8.x everywhere) and bump only when that LTS drops" policy is also downgrade-safe and simpler in `Directory.Packages.props`. We rejected it because:
161+
162+
- Customers on newer runtimes lose access to perf/feature work that landed in later package majors for packages that *aren't* fully overridden by the in-box shared framework (e.g. `Microsoft.Extensions.Caching.Memory`, `System.Configuration.ConfigurationManager`).
163+
- The simplification is small: one extra conditional `ItemGroup` per runtime-aligned package.
164+
- A coordinated bump when the floor LTS drops is a larger, riskier change than incremental per-TFM updates.
165+
166+
Per-TFM keeps each runtime on its matching band and confines change to one `Update` line at a time.
167+
168+
### Why no upper bounds
169+
170+
Customers can transitively pull in a *higher* major than your reference. NuGet resolves nearest-wins with no warning, so a major that broke API can produce `MissingMethodException`/`TypeLoadException` at runtime rather than a restore-time failure.
171+
172+
An upper bound (e.g. `Version="[8.0.0, 10.0.0)"`) would convert that runtime failure into a restore-time `NU1107` and is the only way to guarantee compatibility. We still avoid it because:
173+
174+
- It blocks customers from rolling forward to fix CVEs or take perf wins in the newer major.
175+
- It propagates conflicts deep into customer dependency graphs that we can't see.
176+
- Microsoft's [library guidance](https://learn.microsoft.com/dotnet/standard/library-guidance/dependencies#nuget-dependency-version-ranges) explicitly says **AVOID upper bounds**, and the foundational Microsoft libraries (EF Core, ASP.NET Core, Aspire, Orleans, the BCL itself, Azure SDK) follow it.
177+
- Exact pins (`[X]`) in Microsoft repos are reserved for host-coupled scenarios: Roslyn analyzers (`Microsoft.CodeAnalysis.*`), MSBuild API consumers (`Microsoft.Build`), VS extensibility. None apply to runtime libraries like SqlClient.
178+
179+
We accept the SemVer-break risk in exchange for not breaking customer rollforward. If a specific package is known to break compatibility at a future major, document it in code review and revisit — don't blanket-bound.
180+
181+
### Downgrade direction matters
182+
183+
| Your transitive | Customer's direct | Result |
184+
|---|---|---|
185+
| Higher | Lower | **NU1605 error** (we cause this) |
186+
| Lower | Higher | Clean restore, customer's wins |
187+
| Equal | Equal | Clean |
188+
189+
The asymmetry is the entire reason per-TFM matching works: it makes us the *lower* or *equal* for any customer who has aligned their own references with their runtime.
190+
191+
## Checklist before changing a package version
192+
193+
1. Is the package in the runtime-aligned list above? → use per-TFM conditional `PackageVersion Update`, latest minor/patch in each band.
194+
2. Is it independent? → single `PackageVersion`, latest stable.
195+
3. Is it a polyfill? → conditional `PackageReference` only on TFMs that need it, latest stable.
196+
4. Always pick the latest available minor/patch within the chosen major. Don't carry forward a stale minor when bumping or adding.
197+
5. Prefer Central Package Management (`Directory.Packages.props`) over per-project versions.
198+
6. Never use exact-version (`[X]`) or upper-bound (`[X, Y)`) ranges on `PackageReference` unless you have a documented compatibility reason.
199+
200+
## Sources
201+
202+
- [NU1605 — Detected package downgrade](https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu1605)
203+
- [NuGet Package versioning — version ranges](https://learn.microsoft.com/nuget/concepts/package-versioning#version-ranges)
204+
- [NuGet dependency resolution](https://learn.microsoft.com/nuget/concepts/dependency-resolution)
205+
- [Library guidance — Dependencies](https://learn.microsoft.com/dotnet/standard/library-guidance/dependencies)
206+
- [Library guidance — Cross-platform targeting](https://learn.microsoft.com/dotnet/standard/library-guidance/cross-platform-targeting)

0 commit comments

Comments
 (0)