Versions of referenced projects and the behavior of dotnet pak (CLI)

 

~ 3 min read.

Using nuget packages is a pretty much standard practice in most companies, especially if there are many teams involved. At some point you need to create internal Nuget packages that other teams are going to use, but of course inside your solution you will reference the published project directly when you can.

The story goes

Once upon a time there was a solution structure that was happily buildable in Visual Studio. A solution structure, just like the one we see below:

Products.sln
 |- Common (1.0.0)
 |- Common.Services (1.0.1)
    |- Common.csproj

 |- Product.Common
    |- Common.csproj
 |- Product.Services
    |- Product.Common.csproj
	|- Common.Services.csproj

but then the Business Requirements enforced the Commons to move, and time of “separation of concerns”, or “sock” as it was popular between assemblies, arrived. After the “sock” nothing was the same:

Common.sln
 |- Common (1.0.0)
 |- Common.Services (1.0.1)
    |- Common.csproj

Products.sln
 |- Product.Common
    |- Common 1.0.0
 |- Product.Services
    |- Product.Common.csproj
	|- Common.Services (1.0.1)

There just wasn’t one solution anymore, new references had to be made.

Warning this story doesn’t have a happy ending

Kid, want some packages?

.NET Core CLI defines pak as a command that will produce a *.nupkg file, so let’s try creating some packages.

Common.sln

In Common project we are free of dependencies so we can pak and push the package wright away:

dotnet pak -c Release -p:Version=1.0.0
dotnet nuget push Common.1.0.0 -k your_nuget_api_token -s https://api.nuget.org/v3/index.json

That was easy, now let’s try publishing Common.Services

dotnet pak -c Release -p:Version=1.0.0
dotnet nuget push Common.Services.1.0.0 -k your_nuget_api_token -s https://api.nuget.org/v3/index.json

Also easy, but darn it, you forgot a feature to implement and it’s a small one, so you code it quickly, test it, up the version and pak ‘n’ push:

dotnet pak -c Relase -p:Version=1.0.1
dotnet nuget push Common.Services.1.0.1 -k your_nuget_api_token -s https://api.nuget.org/v3/index.json

Products.sln

In Porducts.Common we need to add the package reference to: Common package and then we can pak ‘n’ push:

dotnet pak -c Release -p:Version=1.0.0
dotnet nuget push Products.Common.1.0.0 -k your_nuget_api_token -s https://api.nuget.org/v3/index.json

For Products.Services we need to add the package reference to: Common.Services 1.0.1:

dotnet pak -c Release -p:Version=1.0.0
dotnet nuget push Products.Services.1.0.0 -k your_nuget_api_token -s https://api.nuget.org/v3/index.json

Unfortunately the second command is never going to happen, since you are going to witness dotnet pak failing to retrieve package: Common 1.0.1 which is completely normal since that package doesn’t exist, but why does dotnet pak is trying to retrieve it. Well as explained here specifying version in dotnet pak via -p:Version parameter, or as a csproj version tag is going to override version of referenced projects, thus leaving you with a need for a non-existing package.

Explanation: Common.Services.nuspec contains a package dependency to: Common 1.0.1 which was generated when you created Common.Services package because the version of main project will override the references project versions, and since Common.Services has a project dependency to Common you will end up with a faulty nuspec.

Curious case of the missing package

The best workaround for this, or the one I liked the most, is usage of conditions inside of csproj file. To be more precise we will setup 2 conditions: one for Debug and one for Release mode. In Debug mode we would like to use project reference, in Release mode we will use the package reference to satisfy dotnet pak.

<ItemGroup Condition="$(Configuration) == Debug">
  <ProjectReference Include="..\Common\Common.Services.csproj" />
</ItemGroup>
  
<ItemGroup Condition="$(Configuration) != Debug">
  <PackageReference Include="Common.Services" Version="1.0.1" />
</ItemGroup>

Voila! Now you can debug and publish, but we still have to maintain the Release mode. Wouldn’t be nice if we could always get the latest version of Common package? If that is something that works for you it can be achieved with small update using Nuget wildcards

<ItemGroup Condition="$(Configuration) == Debug">
  <ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
  
<ItemGroup Condition="$(Configuration) != Debug">
  <PackageReference Include="ProjectReferences.Common" Version="1.*" />
</ItemGroup>

Note: Setting the version value to: 1.* will get the latest version of the package that has a major version equal to 1, if we publish Common package 2.0 we will need to update the csproj file also. The value of the version tag then should be: 2.*.

Major versions must match!

This caveat exists because of another issue that is stopping us from using floating version like this: [1*] which would enable us to resolve packages that have major versions bigger then 1.

Conclusion

As you have been warned this story doesn’t have a happy ending, and the story still goes on, the assemblies still talk about the sock although younger generation have started referring to it as suck. If you want to reproduce this issue yourself your can use the this repo.