7

I have a solution containing several projects. Let's say PackageA and PackageB, where PackageB depends on PackageA with a ProjectReference.
Each project is set to also output a NuGet package on build. This process itself works perfectly but I am unable to specify a package version-range for individual builds. E.g. I'd like to restrict PackageB to only refer to PackageA version 1.0.* (patch steps).

<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
  <PropertyGroup
    <TargetFrameworks>netstandard2.0;netcoreapp2.0;net46</TargetFrameworks>

    <RootNamespace>PackageB</RootNamespace>
    <Company>MyCompany</Company>
    <Authors>John Doe</Authors>
    <Description>This package depends on a specific version of PackageA.</Description>
    <Version>1.1.0</Version>
    <Copyright>Copyright © 2018 John Doe</Copyright>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\PackageA\PackageA.csproj" />
  </ItemGroup>
</Project>

MSBuild seems to ignore any Version="1.0.*" or AllowVersion="1.0.*" arguments within the ProjectReference tag.
Is there a possibility to specify a version range without breaking the ProjectReference or using PackageReference?

Aeon
  • 81
  • 1
  • 8

5 Answers5

7

Short Answer

No, there is no way to limit a project reference by a version attribute of that project.

Longer Answer

If you want your dependent package to vary independently from its dependency and limit the range of changes it will depend upon, you are very much in need of using a package reference rather than a project reference (yes, even if those projects are in the same solution).

Project Reference Now

When you reference a project, you're making a declaration to your IDE that you want to include the referenced projects design time state in your dependent projects design time state so that you can use it and see changes to it in your IDE before it's built. When your dependent project is built, its dependency is built too. So, A project reference is always a latest-version reference. You cannot reference a previous version of a project, but you can reference the versioned result of a project that was built previously.

Packing a Project Reference

In line with project references being built when the dependent project is built, when you pack a project with a dependency upon another project using a project reference, dotnet pack and nuget pack assume that you're going to also be packing each of those projects as packages as well, and writes the project reference as a package dependency at the same version of the dependent project package. So, if you pack projB @ v1.2.3 the package will have a dependency reference to projA @ v1.2.3. If you don't pack projA @ v1.2.3 or you don't publish that package (because maybe there weren't any changes to it), consumers of projB @ v1.2.3 will fail the install because nuget won't find projA @ v1.2.3. If you're going to insist on using project references for packages, those referenced projects should also be packages that are versioned with their host (whether they change or not).

A minor exception to the above reference rule

The exception to project references listed as package dependencies of the same version as the host is a project reference that has its assets marked as private. In those situations you either need to create a build target that will include those assets in the package or have some other convention in place to deliver the dependency to the runtime. Using the private assets route does not allow you to do what you're asking, but it is an exception to the rule of project reference becoming a LISTED dependency of your package.

Josh Gust
  • 4,102
  • 25
  • 41
3

Existing NuGet targets don't support this directly. A couple of issues on GitHub (1, 2) requesting this functionality have been open for years. However, with a bit of MSBuild item trickery, I was able to 'extend' ProjectReference with two attributes, PackageVersion and ExactVersion:

<!-- MyProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <ProjectReference Include="..\MyProject1\MyProject1.csproj" PackageVersion="[1.1.0, 2.0.0)" />
    <ProjectReference Include="..\MyProject2\MyProject2.csproj" ExactVersion="true" />
    <ProjectReference Include="..\MyProject3\MyProject3.csproj" />
  </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 '@(_ProjectReferencesWithVersions)' == '@(_ProjectReferenceWithExplicitPackageVersion)'">
        <ProjectVersion>@(_ProjectReferenceWithExplicitPackageVersion->'%(PackageVersion)')</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      <_ProjectReferenceWithReassignedVersion Include="@(_ProjectReferencesWithVersions)"
        Condition="'%(Identity)' != '' And '@(_ProjectReferencesWithVersions)' == '@(_ProjectReferenceWithExactPackageVersion)'">
        <ProjectVersion>[@(_ProjectReferencesWithVersions->'%(ProjectVersion)')]</ProjectVersion>
      </_ProjectReferenceWithReassignedVersion>
      <_ProjectReferencesWithVersions Remove="@(_ProjectReferenceWithReassignedVersion)" />
      <_ProjectReferencesWithVersions Include="@(_ProjectReferenceWithReassignedVersion)" />
    </ItemGroup>
  </Target>
  ...
</Project>

Given package versions specified in other projects like this

<!-- ..\MyProject1\MyProject1.csproj -->
<!-- ..\MyProject2\MyProject2.csproj -->
<!-- ..\MyProject3\MyProject3.csproj -->
<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Version>1.1.3</Version>
  </PropertyGroup>
  ...
</Project>

the generated MyProject.nuspec file will contain the following dependencies:

<?xml version="1.0" encoding="utf-8" ?>
<package>
  <metadata>
    <dependencies>
      <group targetFramework="...">
        <dependency id="MyProject1" version="[1.1.0, 2.0.0)" />
        <dependency id="MyProject2" version="[1.1.3]" />
        <dependency id="MyProject3" version="1.1.3" />
      </group>
    </dependencies>
  </metadata>
</package>

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

Anton Tykhyy
  • 19,370
  • 5
  • 54
  • 56
  • Thank you, this worked as a Directory.Build.targets with some massaging. I had to remove the Sdk attribute and restart VS to fix an MSBuild error. – carlin.scott Aug 11 '22 at 15:25
1

As far as i know it's not possible with ProjectReference, however there are some open issues in this topic on Github, so it might happen that they will implement it sometime.

But for now this functionality is only enabled on PackageReference. Docs.

Péter Csajtai
  • 878
  • 6
  • 9
0

Well let's think that through shall we? The project may have a version number embedded in in it somewhere, but it's likely to be the latest or previous version, which might not even build, and there's no guarantee that a subsequent build step won't update that value. The point at which a build system produces a versioned artifact is near the end of the build, usually the last step, which is normally the packaging or publishing step.

If your project must limit version ranges for any of its dependencies, it should take dependencies on other packages, not the projects that build them. This provides a natural asynchronous set of workflows to feed into a single product.

If you want the convenience of having dependencies built to their latest, then you must keep all the projects in sync with each other wrt compatibility. Project dependencies really only make sense for developer builds, not CI builds.

One thing you should never do, is produce two different packages with the same version numbers. Visual Studio projects are broken by design in the area of versioning, as they default to using a static version string that must be set prior to the build. If you happen to forget to bump that number, you will violate this semantic versioning rule.

Even if the Nuget/VS devs give you what you are asking for, it's not a good solution. What if the the currently checked out project is for a version outside of the specified range? Assuming the devs can figure out what code to check-out of revision control, is that really what you want to happen on your dev box? Any solution they come up with will be complex and prone to errors. Even if you've got the version checked-out, Nuget can't know you didn't make a breaking change to it.

It's better to run independent pipelines of code, review, build, package, test and publish, using only published packages as dependencies.

jwdonahue
  • 6,199
  • 2
  • 21
  • 43
  • Maybe I'm a bit late to the party, but are you suggesting to use PackageReferences for dependencies that are available even inside the same solution? I'm currently struggling with the same situation but using packages for a project that sits on the same solution seems like an unnecessary indirection. – marcofo88 May 03 '19 at 14:27
  • If it is and will remain unnecessary, don't do it. This not a very good forum to discuss this, but I do tend to avoid all encompassing solutions, because it can be a lot of work disentangling them. I like the one product, one test suite solution. I take dependencies on libraries (products) via package reference. Gives me the freedom to evolve a library as needs arise, without the stiffness involved in having to update all the consuming projects, just to get it to build. When developing a library, my consumer is the unit tests. All others wait for the package. – jwdonahue May 14 '19 at 00:41
0

Are you basing your question on how NodeJS versioning works (^ and ~)? In .NET that's not possible, and not necessary.

NodeJS needs this because, you know, it's javascript. Since javascript doesn't have strict type-checking, you need some way of verifying whether packages are compatible with each other. Some properties, methods might or might not exist on certain objects. So the only way the build system (node) can verify this is through the package version selectors.

As I said, in .NET we don't need this, because it's a strict programming language. If a field, property or method doesn't exist on a class, the project simply won't build.

Pieterjan
  • 2,738
  • 4
  • 28
  • 55