Skip to content
Open
Show file tree
Hide file tree
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
41 changes: 33 additions & 8 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,54 @@
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>

<!--
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!

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.

That language is an editorialization from the 🤖. The strongest statement I see is:

Avoid specifying an upper bound to version ranges to packages you don't own unless you know of a compatibility problem. Upper bounds to version ranges harm adoption, discourage consumers from getting valuable updates to dependencies, and in some cases may lead them to use unsupported versions of dependencies.

But it is interesting to see that EFCore does use upper bounds. I think the mitigating circumstances are that we're not widely consumed by other libraries and we do own the other packages, so the chance of negatively impacting version resolution is probably very small. Also curious to hear if EFCore has ever run into any issues.

Copy link
Copy Markdown
Member

@cheenamalhotra cheenamalhotra Jun 5, 2026

Choose a reason for hiding this comment

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

@mdaigle I think its important to think about the usecase that we have today and that is the Azure extension Package that will no longer be backwards compatible with MDS 7.0.x if this PR makes in: #4200.

How would you expect customers to know to not use a future version of Azure extension package (2.x) with MDS 7.0 (because it would change the dependency tree massively and require upgrading all related application dependencies for some customers) but it is compatible with MDS 7.1??

Azure extension is not a driver's transitive dependency but an extension package that customers would themselves reference.

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.

That's why I'd like to use a different versioning scheme. If all the packages are versioned together then there's no ambiguity.

I think I'm ok to proceed with this. Paul's examples show that there is precedence for the pattern. I wanted to make sure that we don't have special and unexpected behavior.

newer minor/major version.

Because the ceiling is derived from the floor, no separate NextVersion lookup is needed
here — the range is self-contained regardless of what pipeline or developer scenario
computed the floor.
-->
<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.

<SniVersionRange>[$(SniVersion), $(SniVersion.Split('.')[0]).$([MSBuild]::Add($(SniVersion.Split('.')[1]), 1)).0)</SniVersionRange>
</PropertyGroup>

<PropertyGroup Condition="'$(ReferenceType)' == 'Package'">
<SqlServerVersionCeiling>$(SqlServerPackageVersion.Trim().Split('.')[0]).$([MSBuild]::Add($(SqlServerPackageVersion.Trim().Split('.')[1]), 1)).0</SqlServerVersionCeiling>
<LoggingVersionCeiling>$(LoggingPackageVersion.Trim().Split('.')[0]).$([MSBuild]::Add($(LoggingPackageVersion.Trim().Split('.')[1]), 1)).0</LoggingVersionCeiling>
<AbstractionsVersionCeiling>$(AbstractionsPackageVersion.Trim().Split('.')[0]).$([MSBuild]::Add($(AbstractionsPackageVersion.Trim().Split('.')[1]), 1)).0</AbstractionsVersionCeiling>
<SqlClientVersionCeiling>$(SqlClientPackageVersion.Trim().Split('.')[0]).$([MSBuild]::Add($(SqlClientPackageVersion.Trim().Split('.')[1]), 1)).0</SqlClientVersionCeiling>
<AzureVersionCeiling>$(AzurePackageVersion.Trim().Split('.')[0]).$([MSBuild]::Add($(AzurePackageVersion.Trim().Split('.')[1]), 1)).0</AzureVersionCeiling>
<AkvProviderVersionCeiling>$(AkvProviderPackageVersion.Trim().Split('.')[0]).$([MSBuild]::Add($(AkvProviderPackageVersion.Trim().Split('.')[1]), 1)).0</AkvProviderVersionCeiling>
</PropertyGroup>

<!-- ===================================================================== -->
<!-- Driver Packages -->

<!-- The driver packages need version numbers when we build via Package references. -->
<ItemGroup Condition="'$(ReferenceType)' == 'Package'">
<PackageVersion
Include="Microsoft.SqlServer.Server"
Version="$(SqlServerPackageVersion)" />
Version="[$(SqlServerPackageVersion), $(SqlServerVersionCeiling))" />
<PackageVersion
Comment thread
paulmedynski marked this conversation as resolved.
Include="Microsoft.Data.SqlClient.Internal.Logging"
Version="$(LoggingPackageVersion)" />
Version="[$(LoggingPackageVersion), $(LoggingVersionCeiling))" />
<PackageVersion
Include="Microsoft.Data.SqlClient.Extensions.Abstractions"
Version="$(AbstractionsPackageVersion)" />
Version="[$(AbstractionsPackageVersion), $(AbstractionsVersionCeiling))" />
<PackageVersion
Include="Microsoft.Data.SqlClient"
Version="$(SqlClientPackageVersion)" />
Version="[$(SqlClientPackageVersion), $(SqlClientVersionCeiling))" />
<PackageVersion
Include="Microsoft.Data.SqlClient.Extensions.Azure"
Version="$(AzurePackageVersion)" />
Version="[$(AzurePackageVersion), $(AzureVersionCeiling))" />
<PackageVersion
Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider"
Version="$(AkvProviderPackageVersion)" />
Version="[$(AkvProviderPackageVersion), $(AkvProviderVersionCeiling))" />
</ItemGroup>

<!-- ===================================================================== -->
Expand Down Expand Up @@ -108,8 +133,8 @@
<!-- SqlClient Dependencies -->

<ItemGroup>
<PackageVersion Include="Microsoft.Data.SqlClient.SNI" Version="6.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient.SNI.runtime" Version="6.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient.SNI" Version="$(SniVersionRange)" />
<PackageVersion Include="Microsoft.Data.SqlClient.SNI.runtime" Version="$(SniVersionRange)" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.16.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.16.0" />
<PackageVersion Include="System.Buffers" Version="4.6.1" />
Expand Down
3 changes: 3 additions & 0 deletions eng/pipelines/dotnet-sqlclient-ci-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ stages:
buildConfiguration: ${{ parameters.buildConfiguration }}
debug: ${{ parameters.debug }}
dotnetVerbosity: ${{ parameters.dotnetVerbosity }}
loggingArtifactsName: $(loggingArtifactsName)
loggingPackageVersion: $(loggingPackageVersion)
referenceType: ${{ parameters.referenceType }}
# When building Abstractions via packages, we must depend on the Logging
# package.
${{ if eq(parameters.referenceType, 'Package') }}:
Expand Down
67 changes: 58 additions & 9 deletions eng/pipelines/jobs/pack-abstractions-package-ci-job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ parameters:
- 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.

#
# This is used when the referenceType is 'Package'.
- name: loggingArtifactsName
type: string
default: Logging.Artifacts

# The Logging package version to depend on.
#
# This is used when the referenceType is 'Package'.
- name: loggingPackageVersion
type: string
default: ''

# The C# project reference type to use when building and packing the packages.
- name: referenceType
type: string
default: Project
values:
# Reference sibling packages as NuGet packages.
- Package
# Reference sibling packages as C# projects.
- Project

jobs:

- job: pack_abstractions_package_job
Expand Down Expand Up @@ -102,21 +126,46 @@ jobs:
- pwsh: 'Get-ChildItem Env: | Sort-Object Name'
displayName: '[Debug] Print Environment Variables'

# For Package reference builds, we must first download the dependency
# package artifacts.
- ${{ if eq(parameters.referenceType, 'Package') }}:
- task: DownloadPipelineArtifact@2
displayName: Download Logging Package Artifacts
inputs:
artifactName: ${{ parameters.loggingArtifactsName }}
targetPath: $(Build.SourcesDirectory)/packages

# Install the .NET SDK.
- template: /eng/pipelines/steps/install-dotnet.yml@self
parameters:
debug: ${{ parameters.debug }}

# Create the NuGet packages.
- task: DotNetCoreCLI@2
displayName: Create NuGet Package
inputs:
command: pack
packagesToPack: $(project)
configurationToPack: ${{ parameters.buildConfiguration }}
packDirectory: $(dotnetPackagesDir)
verbosityToPack: ${{ parameters.dotnetVerbosity }}
buildProperties: AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }}
#
# When referenceType is Package, we must pass ReferenceType and the
# dependency version so that Directory.Packages.props applies version
# ranges to sibling package dependencies.
- ${{ if eq(parameters.referenceType, 'Package') }}:
- task: DotNetCoreCLI@2
displayName: Create NuGet Package
inputs:
command: pack
packagesToPack: $(project)
configurationToPack: ${{ parameters.buildConfiguration }}
packDirectory: $(dotnetPackagesDir)
verbosityToPack: ${{ parameters.dotnetVerbosity }}
buildProperties: AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }};ReferenceType=Package;LoggingPackageVersion=${{ parameters.loggingPackageVersion }}

- ${{ else }}:
- task: DotNetCoreCLI@2
displayName: Create NuGet Package
inputs:
command: pack
packagesToPack: $(project)
configurationToPack: ${{ parameters.buildConfiguration }}
packDirectory: $(dotnetPackagesDir)
verbosityToPack: ${{ parameters.dotnetVerbosity }}
buildProperties: AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }}

# Publish the NuGet packages as a named pipeline artifact.
- task: PublishPipelineArtifact@1
Expand Down
43 changes: 33 additions & 10 deletions eng/pipelines/jobs/pack-azure-package-ci-job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ parameters:
type: string
default: Logging.Artifacts

# The Abstractions package verion to depend on.
# The Abstractions package version to depend on.
#
# This is used when the referenceType is 'Package'.
- name: abstractionsPackageVersion
type: string

# The Logging package version to depend on.
#
# This is used when the referenceType is 'Package'.
- name: loggingPackageVersion
type: string
default: ''

# The name of the pipeline artifacts to publish.
- name: azureArtifactsName
type: string
Expand Down Expand Up @@ -151,15 +158,31 @@ jobs:
debug: ${{ parameters.debug }}

# Create the NuGet packages.
- task: DotNetCoreCLI@2
displayName: Create NuGet Package
inputs:
command: pack
packagesToPack: $(project)
configurationToPack: ${{ parameters.buildConfiguration }}
packDirectory: $(dotnetPackagesDir)
verbosityToPack: ${{ parameters.dotnetVerbosity }}
buildProperties: AzurePackageVersion=${{ parameters.azurePackageVersion }};AzureAssemblyFileVersion=${{ parameters.azureAssemblyFileVersion }}
#
# When referenceType is Package, we must pass ReferenceType and the
# dependency versions so that Directory.Packages.props applies version
# ranges to sibling package dependencies.
- ${{ if eq(parameters.referenceType, 'Package') }}:
- task: DotNetCoreCLI@2
displayName: Create NuGet Package
inputs:
command: pack
packagesToPack: $(project)
configurationToPack: ${{ parameters.buildConfiguration }}
packDirectory: $(dotnetPackagesDir)
verbosityToPack: ${{ parameters.dotnetVerbosity }}
buildProperties: AzurePackageVersion=${{ parameters.azurePackageVersion }};AzureAssemblyFileVersion=${{ parameters.azureAssemblyFileVersion }};ReferenceType=Package;LoggingPackageVersion=${{ parameters.loggingPackageVersion }};AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }}

- ${{ else }}:
- task: DotNetCoreCLI@2
displayName: Create NuGet Package
inputs:
command: pack
packagesToPack: $(project)
configurationToPack: ${{ parameters.buildConfiguration }}
packDirectory: $(dotnetPackagesDir)
verbosityToPack: ${{ parameters.dotnetVerbosity }}
buildProperties: AzurePackageVersion=${{ parameters.azurePackageVersion }};AzureAssemblyFileVersion=${{ parameters.azureAssemblyFileVersion }}

# Publish the NuGet packages as a named pipeline artifact.
- task: PublishPipelineArtifact@1
Expand Down
27 changes: 27 additions & 0 deletions eng/pipelines/stages/build-abstractions-package-ci-stage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ parameters:
- detailed
- diagnostic

# The name of the Logging pipeline artifacts to download.
#
# This is used when the referenceType is 'Package'.
- name: loggingArtifactsName
type: string
default: Logging.Artifacts

# The Logging package version to depend on.
#
# This is used when the referenceType is 'Package'.
- name: loggingPackageVersion
type: string
default: ''

# The C# project reference type to use when building and packing the packages.
- name: referenceType
type: string
default: Project
values:
# Reference sibling packages as NuGet packages.
- Package
# Reference sibling packages as C# projects.
- Project

stages:

- stage: build_abstractions_package_stage
Expand Down Expand Up @@ -141,3 +165,6 @@ stages:
- test_abstractions_package_job_windows
- test_abstractions_package_job_macos
dotnetVerbosity: ${{ parameters.dotnetVerbosity }}
loggingArtifactsName: ${{ parameters.loggingArtifactsName }}
loggingPackageVersion: ${{ parameters.loggingPackageVersion }}
referenceType: ${{ parameters.referenceType }}
2 changes: 2 additions & 0 deletions eng/pipelines/stages/build-azure-package-ci-stage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,6 @@ stages:
- test_azure_package_job_windows_integration
- test_azure_package_job_macos
dotnetVerbosity: ${{ parameters.dotnetVerbosity }}
loggingArtifactsName: ${{ parameters.loggingArtifactsName }}
loggingPackageVersion: ${{ parameters.loggingPackageVersion }}
referenceType: ${{ parameters.referenceType }}
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,16 @@
<Error Condition="'$(_SqlServerPackageVersionTrimmed)' == ''"
Text="SqlServerPackageVersion is required for SqlClient packaging. When packing via build.proj, set -p:PackageVersionSqlServer=&lt;version&gt;; when packing the project directly, set -p:SqlServerPackageVersion=&lt;version&gt;; otherwise ensure the SqlServer Versions.props import can resolve a value." />

<!-- Expand the SqlClientPackNuspec template with the computed package versions. -->
<!-- Expand the SqlClientPackNuspec template with computed version ranges. -->
<PropertyGroup>
<_SqlClientPackNuspecExpandedText>$([System.IO.File]::ReadAllText('$(SqlClientPackNuspecTemplatePath)').Replace('$AbstractionsPackageVersion$','$(AbstractionsPackageVersion)').Replace('$LoggingPackageVersion$','$(LoggingPackageVersion)').Replace('$SqlServerPackageVersion$','$(SqlServerPackageVersion)'))</_SqlClientPackNuspecExpandedText>
<!-- Compute version ranges: [floor, floor.Major.(floor.Minor+1).0) — ceiling derived from floor.
Use trimmed values to guard against whitespace passed via -p: arguments. -->
<_AbstractionsVersionRange>[$(_AbstractionsPackageVersionTrimmed), $(_AbstractionsPackageVersionTrimmed.Split('.')[0]).$([MSBuild]::Add($(_AbstractionsPackageVersionTrimmed.Split('.')[1]), 1)).0)</_AbstractionsVersionRange>
<_LoggingVersionRange>[$(_LoggingPackageVersionTrimmed), $(_LoggingPackageVersionTrimmed.Split('.')[0]).$([MSBuild]::Add($(_LoggingPackageVersionTrimmed.Split('.')[1]), 1)).0)</_LoggingVersionRange>
<_SqlServerVersionRange>[$(_SqlServerPackageVersionTrimmed), $(_SqlServerPackageVersionTrimmed.Split('.')[0]).$([MSBuild]::Add($(_SqlServerPackageVersionTrimmed.Split('.')[1]), 1)).0)</_SqlServerVersionRange>
</PropertyGroup>
<PropertyGroup>
<_SqlClientPackNuspecExpandedText>$([System.IO.File]::ReadAllText('$(SqlClientPackNuspecTemplatePath)').Replace('$AbstractionsVersionRange$','$(_AbstractionsVersionRange)').Replace('$LoggingVersionRange$','$(_LoggingVersionRange)').Replace('$SqlServerVersionRange$','$(_SqlServerVersionRange)').Replace('$SniVersionRange$','$(SniVersionRange)'))</_SqlClientPackNuspecExpandedText>
</PropertyGroup>
<WriteLinesToFile File="$(SqlClientPackNuspecGeneratedPath)"
Lines="$(_SqlClientPackNuspecExpandedText)"
Expand Down
Loading
Loading