Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions docs/standard/library-guidance/dependencies-net.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
ai-usage: ai-assisted
---
# Guidance for .NET library dependency versions

When building libraries that target multiple .NET versions, choosing dependency versions has implications for compatibility, servicing, and ecosystem health. This document outlines the main strategies, their tradeoffs, and recommendations.

---

## **Overview**

Library authors face challenges when deciding which version of a .NET dependency to reference. Newer versions have more API and features, but may require a local redistribution increasing servicing responsibilities of the library and size of the application. The decision impacts:

- **Friction of updates** on older runtimes. Friction might be more changes than are desired by the library directly referenced. This could be the set of changes introduced between major versions of a dependency, the application size due to more app-local dependencies, the application startup performance due to using app-local dependencies without pre-generated native images, etc.
- **Engineering cost** in maintaining the solution. Here engineering cost is the cost of doing work in the latest codebase. This might be managing more complex projects with conditions, the total number of dependencies that need to be updated regularly, the cost of maintaining local source to account for missing features in older dependencies, dealing with ifdefs around inconsistent API/features across versions, etc.

Check failure on line 15 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces

docs/standard/library-guidance/dependencies-net.md:15:423 MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md009.md
- **Servicing cost** in managing supported releases. Here servicing cost is the ongoing cost in keeping release branches building, up to date, and compliant with all supported build tools.

This guidance provides options, tradeoffs, and a decision matrix to help you choose the best approach.
✔️ DO be deliberate in choosing a dependency policy for your library
✔️ DO remove out of support target frameworks from your package.
✔️ CONSIDER choosing an approach that minimizes engineering costs and adjusting based on customer feedback
✔️ CONSIDER changing your approach through the lifecycle of your library
❌ DO NOT assume any policy is incorrect, all policies are technically sound and supported with different tradeoffs for consumption

---

## **Strategies for Dependency Version Selection**

### **Option 1: Latest supported versions**

Reference the latest supported version of the dependency across all target frameworks. For example at the time this document was written when .NET 10.0 is the latest current release a project would reference reference 10.0 packages on `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`.
> **Note:** The lifetime of the latest Short-Term Support (STS) release now aligns with the latest LTS release, so choosing “latest” effectively means choosing the latest supported version regardless of STS or LTS designation.
> **Note:** Not all packages support targeting older frameworks in their latest version. For example: `Microsoft.AspNetCore.Authorization`. These packages must be excluded from this policy and must always follow option 2 when used.

**Pros**

- Simplifies decision-making and aligns with stability expectations.
- Reduces complexity in dependency management.
- Encourages modernization and enables access to new features.

**Cons**

- Users targeting older TFMs will get the latest version's behavior, including any potential breaking changes.
- Larger number of packages to update in comparison to relying on framework provided API.
- May create friction for customers who are not on the latest supported runtime.
- May prevent consumption in enviroments where dependencies are managed by a host application - eg: MSBuild, Visual Studio, Azure Functions V1.

---

### **Option 2: TFM-specific versions**

Reference different dependency versions per Target Framework Moniker (TFM). Don't reference packages when the framework provides the API. For example: 8.0 packages on `net8.0`, 9.0 packages on `net9.0`, and so on.

**Pros**

- Minimizes change for apps running on older runtimes.
- Minimizes app-local libraries app size on older runtimes. Uses runtime optimized library on older runtimes.

**Cons**

- Reduced API available to library which may lead to more complex implementations (polyfills). Polyfills increase the total cost of engineering and servicing.
- Slows innovation in libraries.
- Greater complexity of infrastructure to maintain seperate dependency sets per target framework. Central packagge management and dependabot can be be configured to work with this, but it's challenging to get it right.

---

### **Option 3: Branching**

Reference the latest supported version of the dependency across all target frameworks, but branch your library in sync with target frameworks. This is the same as Option 1, but each time a new framework is added, a new major version / branch is created to allow for updating the major version of dependencies and adding the new target framework.

**Pros**

- Balances compatibility with flexibility for fast-moving areas.
- Encourages modernization and access to new features.
- Simplifies decision-making and aligns with stability expectations.
- Reduces complexity in dependency management.

**Cons**

- Increased infrastructure cost to maintain concurrent branches.
- No new features for folks who want to stay on older dependencies.

---

## **Decision matrix**

| Strategy | Update Friction | Engineering Cost | Servicing Cost |
|---------------------------------------|------------------|------------------|------------------|
| Latest Supported Versions (Option 1) | Moderate | Low | Moderate |
| TFM-Specific Versions (Option 2) | Low | High | Low / Moderate |
| Branching | Low | Low | High |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| Branching | Low | Low | High |
| Branching | Low | High | High |

Copy link
Member Author

@ericstj ericstj Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the engineering cost of the branching solution is low. Here I use "engineering" to mean steady-state engineering cost in main and I contrast that with "servicing cost". Let me know if you can think of a better term to use.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should even give this option. What is an example of a library that would follow this? Where they basically only add new functionality to the latest TFM support? Are there any examples outside of the .NET Team?

Copy link
Member Author

@ericstj ericstj Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the option for any product that leverages branches to control risk. I do think it's important to call this out, since it does relate to plenty of folks asking these questions, and shows customers how we solve this problem. It's also a fine compromise, since it can be done after the fact for an on-demand release (eg: a security bug, for instance).

I can run a scan of nuget to see which packages use branching like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the top 100 non-microsoft packages, the following use branches to manage risk, determined by observing lower major versions published after higher major versions.

AWSSDK.Core
Castle.Core
Moq
AutoMapper
FluentValidation
Npgsql
FluentAssertions
NUnit
NLog
AWSSDK.S3
RabbitMQ.Client
FluentValidation.DependencyInjectionExtensions
Npgsql.EntityFrameworkCore.PostgreSQL
Elasticsearch.Net
AWSSDK.SecurityToken
FluentValidation.AspNetCore
NodaTime
jQuery
MessagePack
AWSSDK.SQS

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That query seems inconsistent with the definition of "Option 3: Branching"

but branch your library in sync with target frameworks. This is the same as Option 1, but each time a new framework is added, a new major version / branch is created to allow for updating the major version of dependencies and adding the new target framework.

The only packages in your list that I know does that are the 2 Npgsql ones.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but folks can be familiar with branching a general strategy, just because they don't use it precisely the way described doesn't mean it's a foreign idea.

It's also important to understand that it's how all the .NET libraries deal with this problem, and what characteristics that creates.

Maybe I can add a note under branching that some .NET packages do this and drop support for past frameworks (to further reduce engineering cost in the current codebase) but how we DON'T recommend that folks do this in general for libraries since it requires consumers to multi-target their dependencies, while normal branching strategies just give folks the option to multi-target dependencies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I can add a note under branching

That sounds good to me. Thanks


---

## **Key tradeoffs**

- **Friction vs. Innovation:** Latest versions offer new features but have more friction for existing applications on older runtimes.
- **Engineering cost:** Configuring multiple dependency groups, dependency updates for those, and managing API gaps can all accumulate to slow down innovation in a fast moving library.
- **Servicing cost:** More package dependencies means more updates. More branches mean more concurrent builds that need to stay healthy. Additional code in the form of polyfills means more LOC with potential bugs.

---

## **Recommended guidance**

- Option 1: Recommended for libraries moving fast and undergoing lots of innovation and feature development. Consumers are more likely to be on the latest framework.
- Option 2: Recommended for libraries that are stable and do not require new features. Could be a transition strategy when a library reaches maturity and stops taking large features.
- Option 3: Recommended for libraries that are part of .NET or receive strong customer feedback that option 1 is limiting consumption or blocking adoption. Could be a transition strategy when Option 1 has too much friction.

---

## **Examples**

### Example 1: Latest supported version

Check failure on line 113 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Headings should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:113 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "### Example 1: Latest supported version"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md022.md
```xml

Check failure on line 114 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Fenced code blocks should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:114 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```xml"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging" Version="10.0.0" />
<PackageReference Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>
```

### Example 2: TFM-specific versions

Check failure on line 124 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Headings should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:124 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "### Example 2: TFM-specific versions"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md022.md
```xml

Check failure on line 125 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Fenced code blocks should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:125 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```xml"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Condition="'$(TargetFramework)' == 'net8.0'" Include="System.IO.Packaging" Version="8.0.0" />
<PackageReference Condition="'$(TargetFramework)' == 'net9.0'" Include="System.IO.Packaging" Version="9.0.0" />
<PackageReference Condition="'$(TargetFramework)' == 'net10.0'" Include="System.IO.Packaging" Version="10.0.0" />
<!-- Note that System.Text.Json is absent as it is provided by the framework -->
</ItemGroup>
```

### Example 3: Branching

Check failure on line 137 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Headings should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:137 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "### Example 3: Branching"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md022.md
Branch: release/8.0
```xml

Check failure on line 139 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Fenced code blocks should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:139 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```xml"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
```

Branch: release/9.0
```xml

Check failure on line 150 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Fenced code blocks should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:150 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```xml"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
```

Branch: release/10.0
```xml

Check failure on line 161 in docs/standard/library-guidance/dependencies-net.md

View workflow job for this annotation

GitHub Actions / lint

Fenced code blocks should be surrounded by blank lines

docs/standard/library-guidance/dependencies-net.md:161 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```xml"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging" Version="10.0.0" />
<PackageReference Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>
```
Loading