Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow an upper limit Version for ProjectReference references in nupkg from dotnet pack to support semver #5556

Open
livarcocc opened this issue Jul 10, 2017 · 55 comments · Fixed by NuGet/NuGet.Client#3097
Assignees
Labels
Category:SeasonOfGiving https://devblogs.microsoft.com/nuget/nuget-season-of-giving/#season-of-giving Functionality:Pack Functionality:Restore Priority:2 Issues for the current backlog. Type:Feature

Comments

@livarcocc
Copy link

From @csMACnz on July 10, 2017 10:20

It would be good to be able to support Semantic Versioning from a csproj ProjectReference, like you can with PackageReference. To do this, upper version limits in nuget packages help a lot. You cannot currently do this with ProjectReferences and dotnet pack.

Steps to reproduce

  • Create two projects, MyReferencedPackage, and MyNewPackage.
  • Add version information for both packages (e.g. version 1.2.3)
  • Reference MyReferencedPackage from MyNewPackage using a ProjectReference attribute.
    • <ProjectReference Include="..\MyReferencedPackage\MyReferencedPackage.csproj" />
  • (modify the reference above somehow yet to be defined)
  • run dotnet pack on both projects.

Expected behavior

project version in nupkg has a version range (e.g. MyReferencedPackage (≥1.2.3 && < 2.0.0))

Actual behavior

project version in nupkg has the built packages version (e.g. MyReferencedPackage (≥1.2.3))

I'm not too worried on the implementation detail of what the xml should look like, but as a for instance:

<ProjectReference Include="..\MyReferencedPackage\MyReferencedPackage.csproj" >
    <MaximumVersion Inclusive="false">2</MaximumVersion>
</ProjectReference>

To produce the reference from the example above of MyReferencedPackage (≥1.2.3 && < 2.0.0)
(Or if Inclusive is true, then MyReferencedPackage (≥1.2.3 && ≤ 2.0.0))

Copied from original issue: dotnet/cli#7113

@mishra14
Copy link
Contributor

@rohit21agrawal Can you please take a peek and add appropriate labels/milestone?

@nkolev92 nkolev92 added this to the Backlog milestone Nov 9, 2017
@bording
Copy link

bording commented Dec 7, 2017

This is something that I would like to see happen as well. Currently, PackageReferences support the full NuGet interval notation, but ProjectReferences don't.

Because of this, the only way to have packages with the intended version range is to fall back to specifying a manually created nuspec, negating most of the benefits of the new project-based packaging.

@rohit21agrawal
Copy link
Contributor

@emgarten do you think restore can start putting these ranges into the assets file?

@bording
Copy link

bording commented Dec 7, 2017

@rohit21agrawal If #4790 (comment) is considered, would it still make sense to be looking to add these to the assets file?

This issue and #4790 are big pain points for me!

@rohit21agrawal
Copy link
Contributor

rohit21agrawal commented Dec 7, 2017

aah yes, i forgot about #4790 . let me tackle that first, we'll design this when we are done with that (or while doing that if it makes more sense).

@cwharris
Copy link

cwharris commented Feb 22, 2018

This is relevant for those developing frameworks. If I could specify semver for pack, I could publish an entire framework worth of libraries in a single command.

dotnet msbuild /t:pack /p:Version=[0.0.0-dev]

where 0.0.0-dev is the package version of all framework packages being packed, and [0.0.0-dev] is the package reference version for all project dependencies.

I'm probably oversimplifying this, but this is what I'm after right now.

@bording
Copy link

bording commented Apr 7, 2018

@rohit21agrawal Now that #4790 is done and this doesn't appear to have been part of it like you suggested it might be, do you have any idea on when this might be prioritized?

@rohit21agrawal rohit21agrawal added the Priority:1 High priority issues that must be resolved in the current sprint. label Apr 30, 2018
@rohit21agrawal
Copy link
Contributor

@nkolev92 this is the issue I was discussing with you this morning, would love your thoughts on this.

@TheXenocide
Copy link

In a similar, but different vein:

On some solutions with several shared components which are developed somewhat independently and reused among various other solutions, in development we use floating versions (e.g. "2.0.100.*") against a local packages directory and internal NuGet server (local packages increment from 5000 up, build server goes from 0-4999) when restoring packages to ensure we use the latest local build first, otherwise the latest from the build server. Build chains use the explicit version from their build chain dependencies when restoring so they aren't particularly impacted by this design; this is primarily employed in the developer experience.

We have found that it would be really nice to be able to use the same floating version logic in from PackageReference Restore transitively for these projects via the Pack-generated nuspec dependencies (even if "take the latest version" was a feature that projects needed to opt-in to). In order to ensure we restore latest we must specify every transitive reference explicitly, which then make them all non-transitive dependencies. The generated nuspecs always resolve to the exact version that was resolved prior to build and the other supported version range functionality always takes the lowest matching version which is the exact opposite of what we want for development purposes against our own packages.

As an aside, it seems with older project formats Visual Studio 2017 still complains about using MSBuild variables for PackageReference versions, which is how we currently control a lot of this in a shared manner. This is just an added inconvenience as we have to rely on other tools to call nuget/msbuild restore via "command line" even while operating within the IDE. This issue appears to go away when all projects in a solution use the new format and we are able to handle most of this issue as long as we always build every solution that generates nupkgs in dependency order, even if it hasn't changed (otherwise some projects/packages report version conflicts).

@jainaashish jainaashish added Priority:2 Issues for the current backlog. Type:Feature and removed Priority:1 High priority issues that must be resolved in the current sprint. labels Jun 29, 2018
@bencyoung
Copy link

We'd find this issue very useful too. We version all the projects in a solution independently and we'd like to be able to specify both a lower version and especially an upper version for project references else it doesn't work well with semantic versioning

If not that, then a way of picking up local projects in the solution via a ProjectReference would be good

@ctaggart
Copy link

ctaggart commented Jul 7, 2018

Is there currently any way to specify exact version = instead of default >= for the project reference?

@rrelyea
Copy link
Contributor

rrelyea commented Nov 17, 2018

@jainaashish @nkolev92 @dsplaisted @nguerrera @livarcocc @Pilchie - we should likely discuss this request in a Monday sync.

@bencyoung
Copy link

For us the ideal config would be just a flag to use SemVer from the current version and then the referencing project wouldn't have to change if the package major version gets bumped. So something like:

<ProjectReference Include="..\MyReferencedPackage\MyReferencedPackage.csproj" MaxVersion="SemVer" />

would cause it to pick the current version from the referenced project and pick the next major version as the exclusive upper limit

@tbolon
Copy link

tbolon commented Apr 9, 2019

We have a similar problem for our versioning: we are building multiple packages using different builds for the same repo.

Projects are referencing each other using ProjectReference, and our build system set the third part of the version number based on azure devops BuildID. We a trying to respect SemVer, in the sense that we update the minor & major version number when breaking or non trivial changes are detected.

By default, the pack command generate dependencies using the three parts of the version (>=1.0.0). The problem in our case is that the "1.0.0" version do not exists by itself, only 1.0.x where x is an arbitrary build number. When building projects, nuget complains about non existing version (warning) : "NU1603: xxx 1.9.52870 depends on yyyy (>= 1.8.0) but yyyy 1.8.0 was not found. An approximate best match of yyyy 1.8.52292 was resolved."

Example:

  • ProjectA (1.0.0) references ProjectB (1.1.0) using ProjectReference
  • We have one build for each project. We clone the entire solution and keep ProjectReference as-is.
  • When building a project, a task updates the .csproj for the project being built to set the third part of the version with the unique buildID. Ex: when building ProjectB we set "1.1.0" => "1.1.5123" ; when building ProjectA, "1.0.0" becomes "1.0.5124" but ProjectB version is not modified.
  • We build the package of the project using msbuild /t:pack.

When editing a nuspec file by hand, I am able to define a package dependency as >=1.0.*. I tried overriding the GetPackageVersionDependsOn target to force the PackageVersion to "1.0", but a padding zero is added in the final nuspec (I think related to the parsing used). We can't define a floating PackageVersion, because it can't be parsed as a valid version by PackTask and raise an error.

I think the solution proposed in #7213 could be used in our case.

Or instead of customizing the ProjectReference element for each project with a ProjectReference, maybe we could allow the dependency project to define how the dependency version range should be generated ? By allowing the PackageVersion to be a range instead of only a version number, or creating a new Property used to define the version range ?

Allowing the PackageVersion to be a range does not seems to be a good idea, as it's also used when building the project itself.

Adding a new property PackageDependencyVersion (defaulting to PackageVersion when not set), and updating the parsing to first try parsing the value as a range, then as a version could allow this usage.

Also, defining a new Property in the dependency project can allow this property to use other properties from the project itself (Version, PackageVersion, etc.) to generate its value: <PackageDependencyRange>[$(VersionPrefix).*,)</PackageDependencyRange>.

Perhaps both solution could be set: first using a new ProjectReference metadata, then using the referenced project properties?

Ultimately, what we want is the possibility to define the dependency version range as "1.8.*" instead of "1.8.0" in the project (ProjectB in my case).

Edit: I just created a small POC with one solution.

@johnwc
Copy link

johnwc commented Jul 2, 2019

Any update on this?

@nkolev92
Copy link
Member

@tibel helped out with a change that would enable some extra customization at pack time even if it does not address the core problem here.

@razor101
Copy link

Building on @nkolev92 and @madelson's approach and a bit of MSBuild item trickery, I was able to 'extend' ProjectReference with two attributes, PackageVersion and ExactVersion:

<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <ProjectReference Include="..\MyOtherProject1\MyOtherProject1.csproj" PackageVersion="[1.1.0, 2.0.0)" />
    <ProjectReference Include="..\MyOtherProject2\MyOtherProject.2csproj" ExactVersion="true" />
  </ItemGroup>
  ...
  <Target Name="UseExplicitPackageVersions" BeforeTargets="GenerateNuspec">
    <ItemGroup>
      <_ProjectReferenceWithExplicitPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.PackageVersion)' != ''" />
      <_ProjectReferenceWithExactPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.ExactVersion)' == 'true'" />
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithExplicitPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>@(_ProjectReferenceWithExplicitPackageVersion->'%(PackageVersion)')</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithExactPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>[@(_ProjectReferencesWithVersions->'%(ProjectVersion)')]</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      <_ProjectReferencesWithVersions Remove="@(_ProjectReferenceWithReassignedVersion)" />
      <_ProjectReferencesWithVersions Include="@(_ProjectReferenceWithReassignedVersion)" />
    </ItemGroup>
  </Target>
  ...
</Project>

This useful target can be put into Directory.Build.targets to cover all projects in your solution.

This is a great work-around for now.

@jonatansver
Copy link

Building on @nkolev92 and @madelson's approach and a bit of MSBuild item trickery, I was able to 'extend' ProjectReference with two attributes, PackageVersion and ExactVersion:

<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <ProjectReference Include="..\MyOtherProject1\MyOtherProject1.csproj" PackageVersion="[1.1.0, 2.0.0)" />
    <ProjectReference Include="..\MyOtherProject2\MyOtherProject.2csproj" ExactVersion="true" />
  </ItemGroup>
  ...
  <Target Name="UseExplicitPackageVersions" BeforeTargets="GenerateNuspec">
    <ItemGroup>
      <_ProjectReferenceWithExplicitPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.PackageVersion)' != ''" />
      <_ProjectReferenceWithExactPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.ExactVersion)' == 'true'" />
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithExplicitPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>@(_ProjectReferenceWithExplicitPackageVersion->'%(PackageVersion)')</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithExactPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>[@(_ProjectReferencesWithVersions->'%(ProjectVersion)')]</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      <_ProjectReferencesWithVersions Remove="@(_ProjectReferenceWithReassignedVersion)" />
      <_ProjectReferencesWithVersions Include="@(_ProjectReferenceWithReassignedVersion)" />
    </ItemGroup>
  </Target>
  ...
</Project>

This useful target can be put into Directory.Build.targets to cover all projects in your solution.

I was trying to modify this so I could add a property called MajorVersion=true with the outcome of
[{CurrentVersion},{NextMajorVersion) but with no success.
Wandering if someone managed to achieve that?

@s-krawczyk
Copy link

s-krawczyk commented Aug 24, 2023 via email

@ronaldbarendse
Copy link

ronaldbarendse commented Aug 24, 2023

I was trying to modify this so I could add a property called MajorVersion=true with the outcome of
[{CurrentVersion},{NextMajorVersion) but with no success.
Wandering if someone managed to achieve that?

I've successfully limited any packaged project references to the next major version (e.g. [1.2.3, 2) if the current version is 1.2.3) by adding the following target (to the Directory.Build.props file in the repository root):

<!-- Use version range on project references (to limit on major version in generated packages) -->
<Target Name="_GetProjectReferenceVersionRanges" AfterTargets="_GetProjectReferenceVersions">
  <ItemGroup>
    <_ProjectReferencesWithVersions Condition="'%(ProjectVersion)' != ''">
      <ProjectVersion>[%(ProjectVersion), $([MSBuild]::Add($([System.Text.RegularExpressions.Regex]::Match('%(ProjectVersion)', '^\d+').Value), 1)))</ProjectVersion>
    </_ProjectReferencesWithVersions>
  </ItemGroup>
</Target>

This is heavily inspired by other examples posted here, but doesn't require adding additional attributes to the PackageReference and automatically calculates the next major version, since it makes total sense from a semantic versioning point of view to always have this as an upper dependency version limit. Especially if you have multiple projects that are versioned together, you want the packaged project dependencies to use at least the same version (because the lowest compatible version will be resolved for transitive dependencies), but not any future major version, as that will likely contain breaking changes and not be compatible.

Since the above example relies on the private _ProjectReferencesWithVersions item group (identified by the underscore prefix), this should be seen as a workaround. So I also agree that this should either be supported on the PackageReference or otherwise exposed as an official 'extension point'.

@kartheekp-ms
Copy link
Contributor

@ltouro
Copy link

ltouro commented Sep 8, 2023

Having an upperbound would be very useful

@w5l
Copy link

w5l commented Jan 18, 2024

The workaround in #5556 (comment) works a charm, the generated NuGet package now has a version range.

However when actually using the generated package, there still is a runtime dependency on the latest version of the project reference. This means I end up with the runtime error:

FileNotFoundException: Could not load file or assembly 'Some.Dependency, Version=1.2.3.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.

Obviously the Some.Dependency.dll is present, but it is an older version, say 1.2.0.

Looking through the build process, the workaround updates the NuGet dependency versions, but the generated project.assets.json and final Some.Dependency.deps.json in the output still depend on version 1.2.3, which seems to cause the runtime issues.

Anyone knows a fix or workaround that does not involve me adding manual binding redirects like the "good old" net framework days?

@Falco20019
Copy link

Falco20019 commented Mar 27, 2024

I gave all the inputs some more love and added some more operation modes. I also added a duplication check to ensure they get not mixed in a way that would result in multiple ones to be added. This allows all combinations as described by https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#version-ranges

  • PackageVersion
    • Uses explicitily THIS version range (independant of the referenced project versions)
  • ExactVersion
    • Limits the reference to EXACTLY that version of the referenced project
  • LimitedMinimumVersion
    • Limits the minimum version to LimitedMinimumVersion
    • Limits the maximum version to the version of the referenced project (included)
    • If MinimumExcluded is true, LimitedMinimumVersion is excluded
  • LimitedMaximumVersion
    • Limits the minimum version to the version of the referenced project (included)
    • Limits the maximum version to LimitedMaximumVersion
    • If MaximumExcluded is true, LimitedMaximumVersion is excluded
  • MinimumExcluded without or empty LimitedMinimumVersion
    • Unlimits the minimum version
    • Limits the maximum version to the version of the referenced project (included)
  • MaximumExcluded without or empty LimitedMaximumVersion
    • Limits the minimum version to the version of the referenced project (included)
    • Unlimits the maximum version
<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <!-- This uses MyOtherProject1 with [1.1.0, 2.0.0) -->
    <ProjectReference Include="..\MyOtherProject1\MyOtherProject1.csproj" PackageVersion="[1.1.0, 2.0.0)" />
    
    <!-- This uses MyOtherProject2 with [x] where x is the project version -->
    <ProjectReference Include="..\MyOtherProject2\MyOtherProject2.csproj" ExactVersion="true" />
    
    <!-- This uses MyOtherProject3 with [1,x] where x is the project version -->
    <ProjectReference Include="..\MyOtherProject3\MyOtherProject3.csproj" LimitedMinimumVersion="1" MinimumExcluded="false" />
    
    <!-- This uses MyOtherProject4 with [x,2) where x is the project version -->
    <ProjectReference Include="..\MyOtherProject4\MyOtherProject4.csproj" LimitedMaximumVersion="2" MaximumExcluded="true" />
    
    <!-- This uses MyOtherProject5 with (,x] where x is the project version -->
    <ProjectReference Include="..\MyOtherProject5\MyOtherProject5.csproj" MinimumExcluded="true" />
    
    <!-- This uses MyOtherProject6 with [x,) where x is the project version -->
    <ProjectReference Include="..\MyOtherProject6\MyOtherProject6.csproj" MaximumExcluded="true" />
  </ItemGroup>
  ...
  <UsingTask TaskName="CheckForDuplicateItems" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />
  
  <Target Name="UseExplicitPackageVersions" BeforeTargets="GenerateNuspec">
    <ItemGroup>
      <!-- Support for LimitedMinimumVersion attribute -->
      <_ProjectReferenceWithLimitedMinimumPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.LimitedMinimumVersion)' != '' OR '%(ProjectReference.MinimumExcluded)' == 'true'" />
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithLimitedMinimumPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>@(_ProjectReferenceWithLimitedMinimumPackageVersion->'%(LimitedMinimumVersion)'),@(_ProjectReferencesWithVersions->'%(ProjectVersion)')</ProjectVersion>
        <MinimumExcluded>@(_ProjectReferenceWithLimitedMinimumPackageVersion->'%(MinimumExcluded)')</MinimumExcluded>
      </_ProjectReferenceWithReassignedVersion>
      
      <!-- Support for LimitedMaximumVersion attribute -->
      <_ProjectReferenceWithLimitedMaximumPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.LimitedMaximumVersion)' != '' OR '%(ProjectReference.MaximumExcluded)' == 'true'" />
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithLimitedMaximumPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>@(_ProjectReferencesWithVersions->'%(ProjectVersion)'),@(_ProjectReferenceWithLimitedMaximumPackageVersion->'%(LimitedMaximumVersion)')</ProjectVersion>
        <MaximumExcluded>@(_ProjectReferenceWithLimitedMaximumPackageVersion->'%(MaximumExcluded)')</MaximumExcluded>
      </_ProjectReferenceWithReassignedVersion>
      
      <!-- Apply MinimumExcluded attributes -->
      <_ProjectReferenceWithReassignedVersion Update="@(_ProjectReferenceWithReassignedVersion)">
        <ProjectVersion Condition="'%(_ProjectReferenceWithReassignedVersion.MinimumExcluded)' == 'true'">(%(ProjectVersion)</ProjectVersion>
        <ProjectVersion Condition="'%(_ProjectReferenceWithReassignedVersion.MinimumExcluded)' != 'true'">[%(ProjectVersion)</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      
      <!-- Apply MaximumExcluded attributes -->
      <_ProjectReferenceWithReassignedVersion Update="@(_ProjectReferenceWithReassignedVersion)">
        <ProjectVersion Condition="'%(_ProjectReferenceWithReassignedVersion.MaximumExcluded)' == 'true'">%(ProjectVersion))</ProjectVersion>
        <ProjectVersion Condition="'%(_ProjectReferenceWithReassignedVersion.MaximumExcluded)' != 'true'">%(ProjectVersion)]</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      
      <!-- Support for ExactVersion attribute -->
      <_ProjectReferenceWithExactPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.ExactVersion)' == 'true'" />
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithExactPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>[@(_ProjectReferencesWithVersions->'%(ProjectVersion)')]</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      
      <!-- Support for PackageVersion attribute -->
      <_ProjectReferenceWithExplicitPackageVersion Include="@(ProjectReference->'%(FullPath)')" Condition="'%(ProjectReference.PackageVersion)' != ''" />
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)" Condition="'%(Identity)' != '' And '@(_ProjectReferenceWithExplicitPackageVersion)' == '@(_ProjectReferencesWithVersions)'">
        <ProjectVersion>@(_ProjectReferenceWithExplicitPackageVersion->'%(PackageVersion)')</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
    </ItemGroup>
      
    <CheckForDuplicateItems
      Items="@(_ProjectReferenceWithExplicitPackageVersion)"
      ItemName="_ProjectReferenceWithExplicitPackageVersion"
      DefaultItemsEnabled="false"
      DefaultItemsOfThisTypeEnabled=""
      PropertyNameToDisableDefaultItems="None"
      MoreInformationLink="None"
      ContinueOnError="false">
    </CheckForDuplicateItems>

    <ItemGroup>
      <!-- Replace all reassigned versions -->
      <_ProjectReferencesWithVersions Remove="@(_ProjectReferenceWithReassignedVersion)" />
      <_ProjectReferencesWithVersions Include="@(_ProjectReferenceWithReassignedVersion)" />
    </ItemGroup>
  </Target>
  ...
</Project>

@atykhyy
Copy link

atykhyy commented Mar 27, 2024

@Falco20019's solution gave me an even simpler and more elegant idea which does not need multiple attributes or duplicate checking:

<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <!-- This uses MyOtherProject1 with [1.1.0, 2.0.0) -->
    <ProjectReference Include="..\MyOtherProject1\MyOtherProject1.csproj" PackageVersion="[1.1.0, 2.0.0)" />
    
    <!-- This uses MyOtherProject2 with [x] where x is the project version -->
    <ProjectReference Include="..\MyOtherProject2\MyOtherProject2.csproj" PackageVersion="[~]" />
    
    <!-- This uses MyOtherProject3 with [1,x] where x is the project version -->
    <ProjectReference Include="..\MyOtherProject3\MyOtherProject3.csproj" PackageVersion="[1,~]" />
    
    <!-- This uses MyOtherProject4 with [x,2) where x is the project version -->
    <ProjectReference Include="..\MyOtherProject4\MyOtherProject4.csproj" PackageVersion="[~,2)" />
    
    <!-- This uses MyOtherProject5 with (,x] where x is the project version -->
    <ProjectReference Include="..\MyOtherProject5\MyOtherProject5.csproj" PackageVersion="(,~]" />
    
    <!-- This uses MyOtherProject6 with [x,) where x is the project version -->
    <!-- (note that in this case PackageVersion attribute is superfluous 
          as this is the default behavior of GenerateNuspec) -->
    <ProjectReference Include="..\MyOtherProject6\MyOtherProject6.csproj" PackageVersion="[~,)" />
  </ItemGroup>

  <Target Name="UseExplicitPackageVersions" BeforeTargets="GenerateNuspec">
    <ItemGroup>
      <_ProjectReferencesWithVersions Condition="'%(FullPath)' != ''">
        <PackageVersion>@(ProjectReference->'%(PackageVersion)')</PackageVersion>
      </_ProjectReferencesWithVersions>
      <_ProjectReferencesWithVersions Condition="'%(Identity)' != '' And '%(PackageVersion)' != ''">
        <ProjectVersion>$([System.String]::new('%(PackageVersion)').Replace('~',%(ProjectVersion)))</ProjectVersion>
      </_ProjectReferencesWithVersions>
    </ItemGroup>
  </Target>
  ...
</Project>

I use ~ as the placeholder for the referenced project's version because it is not a valid character in a SemVer string.

@namerril
Copy link

namerril commented Mar 29, 2024

@atykhyy I am completely baffled how @(ProjectReference->'%(PackageVersion)') even works, as it breaks my understanding of batching, but I tested it, and it seems to correctly apply the metadata of ProjectReference to the correct _ProjectReferencesWithVersions item.

@atykhyy
Copy link

atykhyy commented Mar 29, 2024

@namerril: The fragment

<_ProjectReferencesWithVersions Condition="'%(FullPath)' != ''">
  <PackageVersion>@(ProjectReference->'%(PackageVersion)')</PackageVersion>
</_ProjectReferencesWithVersions>

batches _ProjectReferencesWithVersions and ProjectReference item lists together on FullPath. FullPath being the same as Identity in the former list and a unique metadata in the latter (from which default NuGet targets build the former list), the list @(ProjectReference) in each batch contains just the one item corresponding to the _ProjectReferencesWithVersions item. MSBuild documentation covers this use of task batching, although it is a bit obscure. Here are two other references I found helpful.

@JTeeuwissen
Copy link

JTeeuwissen commented Mar 29, 2024

Is it possible to use this workaround with project references conditional on the target framework as well? I tried to get it to work with one of the previously send build targets but _ProjectReferencesWithVersions seemed to behave differently for these conditional references.

@atykhyy
Copy link

atykhyy commented Mar 30, 2024

@JTeeuwissen It is possible, but it is a bit more involved, because the GenerateNuspec target does not run in a specific target framework. It takes per-TFM information from the assets file, and it is impossible to put version ranges there. One has to pass per-TFM project references to the workaround target. Here is one way to do it:

<Target Name="ReturnProjectReferences" Returns="@(ProjectReference)" />
<Target Name="UseExplicitPackageVersions" BeforeTargets="GenerateNuspec">
  <!-- collect per-TFM project reference items -->
  <MSBuild Projects="$(MSBuildProjectFullPath)" Targets="ReturnProjectReferences"
      Properties="TargetFramework=%(_TargetFramework.Identity)">
    <Output TaskParameter="TargetOutputs" ItemName="_MergedProjectReferences" />
  </MSBuild>
  <!-- select project reference items which specify package versions and clean up duplicates -->
  <ItemGroup>
    <_MergedPackageVersions Include="@(_MergedProjectReferences)"
      Condition="'%(_MergedProjectReferences.PackageVersion)' != ''" 
      KeepMetadata="PackageVersion" KeepDuplicates="false" />
  </ItemGroup>
  <ItemGroup>
    <_ProjectReferencesWithVersions Condition="'%(FullPath)' != ''">
      <PackageVersion>@(_MergedPackageVersions->'%(PackageVersion)')</PackageVersion>
    </_ProjectReferencesWithVersions>
    <_ProjectReferencesWithVersions Condition="'%(Identity)' != '' And '%(PackageVersion)' != ''">
      <ProjectVersion>$([System.String]::new('%(PackageVersion)').Replace('~',%(ProjectVersion)))</ProjectVersion>
    </_ProjectReferencesWithVersions>
  </ItemGroup>
</Target>

Note that this will cause GenerateNuspec to barf if you have different PackageVersion specifications in different TFMs for the same project reference.

@michaelhahn-doka
Copy link

michaelhahn-doka commented Apr 4, 2024

The approaches you all provided work very good for stable versions, but given the fact that I publish preview (x.y.z-alpha.1 when published from a feature branch, x.y.z-beta.1 when published from the develop branch and x.y.z when published on the main branch) versions as well.
Currently all my project references have the following PackageVersion="[8.0.0, 9.0.0)" but when I have ProjectA and ProjectB (dependent on ProjectA) where ProjectA has version 8.2.1-beta.1 and ProjectB also has 8.2.1-beta.1, but in the of the NuGet package the version spec is [8.0.0, 9.0.0), then I get the following error: Unable to find a stable package PackageA with version (>= 8.0.0 && < 9.0.0) since it only accepts stable versions.

Is there anything I could change to also support prerelease versions here?

@Falco20019
Copy link

According to https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1103 and https://learn.microsoft.com/en-us/nuget/concepts/dependency-resolution#floating-versions you would need to use [8-*,9) (or the corresponding equivalent [8.0.0-*,9.0.0) if preferred) to allow pre-releases. I have never used it with the min version not being exactly the one pre-release I needed, but for [8.2.1-*,9) I did already test it successful in the past.

@michaelhahn-doka
Copy link

With that version spec you proposed I always get the following issue: /NuGet.Build.Tasks.Pack.targets(221,5): error : '[8.0.0-*, 9.0.0)' is not a valid version string. when I run the following command: dotnet pack . --include-symbols --include-source /p:PackageVersion=1.2.3-beta.1
So somehow I have an issue in my versioning or there's really a missing configuration/property/target in my csproj/target files.

@JTeeuwissen
Copy link

JTeeuwissen commented Apr 4, 2024

@michaelhahn-doka
8.0.0-* is not valid in nuspec iirc. In csproj it is translated to the current latest prerelease which is inserted in the nuspec. Instead you can try 8.0.0-0, which will resolve to the lowest prerelease.

@michaelhahn-doka
Copy link

@michaelhahn-doka 8.0.0-* is not valid in nuspec iirc. In csproj it is translated to the current latest prerelease which is inserted in the nuspec. Instead you can try 8.0.0-0, which will resolve to the lowest prerelease.

But that would mean, that I only support prerelease but no stable versions right because of the missing *?

@JTeeuwissen
Copy link

But that would mean, that I only support prerelease but no stable versions right because of the missing *?

No, this should allow for stable versions to be used as well.
The * indicates that the lower bound should be the latest prerelease while -0 comes before all other prereleases alphabetically
In either case, the stable version is still included in this range.

@atykhyy
Copy link

atykhyy commented Apr 12, 2024

Right. I don't think it is possible to say "use only beta versions in this range of numerical versions" with SemVer. (Mathematically speaking, SemVer version strings are defined to be completely ordered.) This means that separately versioned 'lineages' of packages like that must have a different set of package names: MyStuff.Abstractions.Beta, MyStuff.Core.Beta and so on, with appropriate dependencies. When doing so, it is convenient to share non-suffixed assembly names and assembly versions (if one even uses assembly versions at all) with the stable lineage, so that MyStuff.Abstractions.Beta package will add MyStuff.Abstractions.dll and so on. If at some point one decides to stabilize e.g. MyStuff.Abstractions.Beta while retaining beta lineages of other packages and free upgradability, one can simply make MyStuff.Abstractions.Beta a shell package which adds a dependency on the stable MyStuff.Abstractions. Assembly names being the same will ensure that projects using the packages will compile and run without issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category:SeasonOfGiving https://devblogs.microsoft.com/nuget/nuget-season-of-giving/#season-of-giving Functionality:Pack Functionality:Restore Priority:2 Issues for the current backlog. Type:Feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.