Skip to content

Use upper-bound version ranges for sibling package dependencies#4337

Open
paulmedynski wants to merge 3 commits into
mainfrom
dev/paul/upper-bounds
Open

Use upper-bound version ranges for sibling package dependencies#4337
paulmedynski wants to merge 3 commits into
mainfrom
dev/paul/upper-bounds

Conversation

@paulmedynski
Copy link
Copy Markdown
Contributor

@paulmedynski paulmedynski commented Jun 3, 2026

Compute [floor, ceiling) ranges for sibling packages so NuGet cannot resolve an incompatible newer minor/major version at restore time.

Changes

  • Directory.Packages.props: Derive *VersionCeiling from *PackageVersion using MSBuild property functions; apply [floor, ceiling) ranges to all sibling PackageVersion items; add centralized SniVersion / SniVersionRange properties.
  • SqlClient csproj: Compute version range properties for nuspec token expansion.
  • SqlClient nuspec: Replace bare version tokens with range tokens ($AbstractionsVersionRange$, $LoggingVersionRange$, $SqlServerVersionRange$, $SniVersionRange$).

How it works

The ceiling is the next minor version (prerelease suffix stripped) derived from the floor:

Floor:   1.1.0-preview1-ci123
Ceiling: 1.2.0
Range:   [1.1.0-preview1-ci123, 1.2.0)

This is self-contained — no separate "next version" lookup is needed. The range prevents NuGet from pulling in a newer minor or major version that could be binary-incompatible.

Pipeline Verification

  • PR runs:
  • CI runs:
  • OneBranch runs:
    • Non-Official: 26155.1
    • Official: Can't trigger until the changes merge to ADO.Net via a PR.

Reviewers should inspect the NuGet package artifacts produced by the above runs to confirm that the sibling dependencies specify the expected version ranges.

Manual Verification

  • dotnet build (direct, net8.0/net9.0) — Project mode ✓, Package mode ✓
  • build.proj -t:Build — Project mode ✓, Package mode ✓
  • build.proj -t:Pack — Project mode ✓, Package mode ✓
  • Produced .nupkg files contain correct [floor, ceiling) dependency ranges for all sibling packages and SNI

Compute [floor, ceiling) ranges for sibling packages so NuGet cannot
resolve an incompatible newer minor/major version at restore time.

- Directory.Packages.props: derive *VersionCeiling from *PackageVersion;
  apply ranges to sibling PackageVersion items; add SniVersionRange
- SqlClient csproj: compute version range properties for nuspec expansion
- SqlClient nuspec: replace bare version tokens with range tokens
Copilot AI review requested due to automatic review settings June 3, 2026 14:40
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Jun 3, 2026
@paulmedynski paulmedynski added the Area\Engineering Use this for issues that are targeted for changes in the 'eng' folder or build systems. label Jun 3, 2026
@paulmedynski paulmedynski moved this from To triage to In progress in SqlClient Board Jun 3, 2026
@paulmedynski paulmedynski added this to the 7.1.0-preview2 milestone Jun 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR changes how Microsoft.Data.SqlClient expresses dependency versions by computing [floor, ceiling) version ranges (ceiling = next minor) for sibling packages and SNI, and emitting those ranges into the produced .nuspec/.nupkg.

Changes:

  • Compute “next-minor” ceilings from existing *PackageVersion / SniVersion values and form [floor, ceiling) ranges.
  • Apply those ranges in Central Package Management (Directory.Packages.props) for sibling package dependencies (Package mode) and SNI.
  • Update the SqlClient pack flow to materialize dependency range tokens into the generated nuspec.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
Directory.Packages.props Adds MSBuild-derived ceiling/range properties and applies [floor, ceiling) ranges to sibling package PackageVersions and SNI.
src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj Computes version ranges during nuspec materialization and substitutes new range tokens.
src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.nuspec Replaces dependency version tokens with range tokens for sibling packages and SNI.

Comment thread src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.nuspec
Comment thread Directory.Packages.props
Comment thread src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.53%. Comparing base (bfbdd30) to head (5060f1a).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4337      +/-   ##
==========================================
- Coverage   66.69%   64.53%   -2.17%     
==========================================
  Files         284      279       -5     
  Lines       43238    66069   +22831     
==========================================
+ Hits        28836    42635   +13799     
- Misses      14402    23434    +9032     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 64.53% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread Directory.Packages.props
-->
<PropertyGroup>
<!-- SNI version (external package, version declared once here) -->
<SniVersion>6.0.2</SniVersion>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I decided to declare the SNI version once here rather than here and in the SqlClient nuspec. It's a pseudo-sibling package, so I felt if warranted this special treatement.

The CI/PR pack jobs for Abstractions and Azure extensions were not
passing ReferenceType=Package to dotnet pack, causing the projects to
use ProjectReference mode for sibling dependencies. NuGet converts
ProjectReferences to bare version dependencies (no ranges), which
defeats the upper-bound version ranges defined in Directory.Packages.props.

Fix:
- pack-abstractions-package-ci-job: Add referenceType, loggingArtifactsName,
  and loggingPackageVersion parameters. When referenceType=Package, download
  Logging artifacts and pass ReferenceType + LoggingPackageVersion to dotnet
  pack buildProperties.
- pack-azure-package-ci-job: Add loggingPackageVersion parameter. When
  referenceType=Package, pass ReferenceType + dependency versions to dotnet
  pack buildProperties.
- Wire the new parameters through build-abstractions-package-ci-stage.yml
  and dotnet-sqlclient-ci-core.yml.
- detailed
- diagnostic

# The name of the Logging pipeline artifacts to download.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Abstractions and Azure build/pack CI jobs were missing some sibling package versions, and weren't using package-mode properly.

@paulmedynski paulmedynski marked this pull request as ready for review June 4, 2026 14:02
@paulmedynski paulmedynski requested a review from a team as a code owner June 4, 2026 14:02
Copilot AI review requested due to automatic review settings June 4, 2026 14:02
@paulmedynski paulmedynski moved this from In progress to In review in SqlClient Board Jun 4, 2026
@paulmedynski paulmedynski enabled auto-merge (squash) June 4, 2026 14:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj Outdated
Comment thread Directory.Packages.props Outdated
Comment thread Directory.Packages.props Outdated
Address Copilot review feedback: guard against whitespace in version
values passed via -p: arguments by trimming before splitting.

- Directory.Packages.props: Add .Trim() before .Split('.') on all
  *PackageVersion properties used in ceiling computation.
- Microsoft.Data.SqlClient.csproj: Use the already-computed
  _*PackageVersionTrimmed properties as inputs to the range strings
  instead of the raw *PackageVersion values.
Comment thread Directory.Packages.props
<!--
Compute version ceilings for sibling packages. The ceiling is the next minor version
(prerelease suffix stripped) derived from the *floor* (XxxPackageVersion), giving a range
like [1.1.0-preview1-ci123, 1.2.0). This prevents NuGet from resolving an incompatible
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The ceiling should be the next major version that can contain breaking changes.

Per SemVer versioning semantics, a minor release is never expected to contain breaking changes for any consumers, and should be backwards-compatible with consuming applications if customers happen to upgrade.

Copy link
Copy Markdown
Contributor Author

@paulmedynski paulmedynski Jun 4, 2026

Choose a reason for hiding this comment

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

Yes I thought about that, but do we actually want to allow dependency across the minor version boundary? Do we intend to release Abstractions 1.1.0 without releasing Azure 1.1.0, for example?

This is a policy question:

  • Do we set our policy such that we release all siblings as minor version bumps together, and only let their patch versions diverge?
  • Your suggestion implies we already must release all siblings as major version bumps together, even if some of them don't contain any major-worthy changes.

@benrr101 @mdaigle - What are your thoughts?

Copy link
Copy Markdown
Contributor

@mdaigle mdaigle Jun 5, 2026

Choose a reason for hiding this comment

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

I'm actually thinking that we don't need to set upper bounds at all. What is the actual use case for explicitly limiting? Why is our suite special in a way that requires upper bounds when the ecosystem discourages them? When nuget goes to resolve the dependency graph, it will already select the lowest version that satisfies all constraints in the graph. This means that as long as we continue to set exact lower bounds, customers will not get even minor version bumps of sibling packages except in certain special situations mentioned below.

Consider:

App {
    MDS 7.0.0 {
        Abstractions >= 1.0.0
    }
}

If we publish Abstractions 1.1.0 (or even 2.0.0), the app will still resolve to 1.0.0.

The only ways a customer could get an incompatible version of a sibling package is:

  • If they take a direct dependency on an incompatible version
    • Customers will have to directly depend on extension packages to e.g. implement their own auth provider. But if we version all of our packages together, it's easy to keep your MDS and extension versions in sync.
    • Still requires an explicit action by the app maintainer. They won't suddenly start resolving a new version just because we released one (unless they use float versions, which is an at-your-own-risk option).

(Can expose customers to behavior changes or runtime errors)

App {
    MDS 7.0.0 {
        Abstractions 1.0.0
    }
    Abstractions 2.0.0
}
  • They take a dependency on another library, which takes a dependency on an incompatible version
    • Maybe someone needs both EFCore and SqlClient in the same repo? It feels unlikely. But in that case, EFCore would also be bringing in a later MDS version and the customer would get a downgrade warning for MDS.
    • To see an issue, would require a library that directly references an extensions package (Abstractions/Logging/etc.)

(gives a nuget warning that direct MDS 7.0.0 dependency is a downgrade from 8.0.0)

App {
    MDS 7.0.0 {
        Abstractions 1.0.0
    }
    EFCore {
        MDS 8.0.0 {
            Abstractions 2.0.0
        }
    }
}

(can fail at runtime)

App {
    MDS 7.0.0 {
        Abstractions 1.0.0
    }
    OtherLibrary 1.0.0 {
        Abstractions 2.0.0
    }
}

The core of my point is that our risks aren't special. They're the same risks any other library has. The ecosystem puts the burden on users to safely manage their package versions. In exchange, it's almost always possible to resolve the package graph without irreconcilable version constraints. Avoiding upper bounds maintains a safety hatch for customers. In case they want/need to take a higher version of a sibling package, they can do so at their own risk.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with @mdaigle - why is MDS special from any other nuget package, and constraining is highly discouraged

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did a little more digging myself:

Industry norm: bare minimum versions (1.2.3 meaning >= 1.2.3). This is what the vast majority of NuGet packages ship with, including most Microsoft packages. The reasoning:

  • NuGet's resolver picks the lowest applicable version, so in practice you usually get exactly what was specified.
  • It gives consumers flexibility to unify transitive versions without conflicts.
  • SemVer is trusted to mean minor/patch bumps won't break you.

Bounded ranges are rare but recommended for tightly-coupled packages. Microsoft's own guidance (the NuGet docs on version ranges) says:

Pattern Use case
1.2.3 (bare) General dependencies — trust SemVer
[1.2.3, 2.0.0) Tightly coupled siblings or plugins where a major bump is breaking
[1.2.3] (exact) Almost never — too restrictive, causes diamond-dependency conflicts

Who uses bounded ranges in practice:

  • ASP.NET Core shared framework packages use [major.minor.*, next-major.0.0) for their internal sibling refs.
  • Azure SDK uses bounded ranges between tightly-versioned sibling libs.
  • EF Core bounds its internal package references.

We're definitely in the "tightly coupled sibling" case, but does that mean we absolutely need upper bounds? Our EF Core friends use it, so perhaps a discussion with them is warranted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Tightly coupled siblings or plugins where a major bump is breaking" - interesting!

@github-project-automation github-project-automation Bot moved this from In review to Waiting for customer in SqlClient Board Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area\Engineering Use this for issues that are targeted for changes in the 'eng' folder or build systems.

Projects

Status: Waiting for customer

Development

Successfully merging this pull request may close these issues.

6 participants