Magnus Rahl
Mar 15, 2022
  3367
(8 votes)

Resolving Nuget dependency conflicts in Project SDK (PackageReference) model

After the release of CMS 12 and Commerce 14 we have received reports from partners about package incompatibility issues when installing and updating our Nuget packages. In this post I'll try to address both the background for why this is happening, as well as give you methods for working around these issues.

I do recommend you read this whole post, but for the impatient here is the TL;DR:

  • The dependency issues aren't specific to Optimizely/Episerver packages, but are made worse by some practices we adopted years ago to balance several aspects of dependencies and breaking changes.
  • The dependency issues aren't actually related to CMS 12 or .NET 5/6, but to the change from old style csproj+packages.config to SDK style projects with PackageReference (which doesn't support classic ASP.NET project and hence hasn't been viable until ASP.NET Core / .NET 5) which resolves transitive dependencies dynamically according to these rules.
  • You can work around the issues by following the hints in the errors and warnings (NU1107 and NU1608). It may take multiple iterations and adding multiple packages before you get to a compatible set.
  • Getting to the latest versions of all packages will often be more complex than staying with the top-most "umbrella" packages like EPiServer.CMS and EPiServer.Commerce which may give you slightly older versions.
  • Multi-project solutions will be more complex, especially if you reference lower level dependencies rater than the "umbrella" packages.
  • Studying the dependency hierarchies and version restrictions of EPiServer.* packages as well as understanding the Nuget depencency resolution rules can help you gain a better understanding for which dependencies you need to add (and which ones you may be able to remove). Some hints about version relationships in the last section (appendix) of this post.

Background I: Shift from packages.config to PackageReference

In the old world, nuget packages and their versions would be listed in packages.config. In this scenario, all packages would be listed, inlcuding transitive dependencies of packages you install (installed packages' dependencies, dependencies of those dependencies and so on). Because of this, the set of packages and versions is unambiguous.

If you want to install another package that shares a dependency with a package you already have installed, you will have to make sure to install a version of that common dependency that is compatible with both. But once you've done that (which is often automatic, e.g. the new package requires a newer version of the dependency, so the dependency is upgraded), it is once again unambigous which verison should be used.

Enter SDK style projects and PackageReference. PackageReference allows you to specify only a top level dependency, and all the transitive dependencies of that package are implicit. To figure out which versions of those packages that are actually used when packages are restored, Nuget has a set of rules it follows.

These rules are somewhat similar to what happened in the old age packages.config when installing, but the result of a dependency resolution is not (automatically) "documented" or "written to lock file" in the same way. The resolution happens by the rules each time there is a restore (e.g. when opening or building the project). So the dependencies have to be unambigous by the resolution rules, which is a stricter / more difficult to fulfill requirements. You may have to resort to add additional dependencies to act as a tiebreak on the resolution rules, manually creating a (partial) lock file.

Background II: Optimizely/Episerver dependency structure

We maintain a large set of packages across many teams. Some packages are more closely related, maintained by the same team and on the same release cycle. Others are on independent release cycles. Overall we are balancing a number of different perspectives through our dependencies. To pick three:

  1. Decoupling and autonomy of teams.
  2. Minimizing number of breaking changes (major versions), for example by using "pubternal" API:s between packages we control.
  3. Modularizaiton and decoupling on package level, depending on simplest possible package (no UI where not required, no web stack where not required).

1 and 3 drive towards splitting packages smaller, creating chains of dependencies. 2. requires more close control of dependency ranges between packages that use pubternal API:s that may break between major verisons - we tie versions 1:1 to know that we can break pubternal APIs without risking incompatibilities. 3. creates a complex dependency graph where there are multiple paths to the same dependency.

Take for example the EPiServer.CMS package, often referred to as the "umbrella" package. Its purpose is to get you the dependencies you need to run a CMS website, with UI. If you don't want the UI, don't run a website (like building a library) etc, you would reference something else. But this is basically the "top" level for a plain CMS project. In the other end is our foundational EPiServer.Framework package. Let's take a look at three different paths between these:

EPiServer.CMS 12.1.0 -> EPiServer.Hosting [12.0.3, 13) -> EPiServer.Framework 12.0.3
EPiServer.CMS 12.1.0 -> EPiServer.CMS.AspNetCore.HtmlHelpers [12.0.3, 13) -> EPiServer.CMS.AspNetCore.Mvc 12.0.3 -> EPiServer.CMS.AspNetCore.Routing 12.0.3 -> EPiServer.CMS.AspNetCore.Templating 12.0.3 -> EPiServer.CMS.AspNetCore 12.0.3 -> EPiServer.CMS.Core 12.0.3 -> EPiServer.Framework 12.0.3
EPiServer.CMS 12.1.0 -> EPiServer.CMS.UI [12.1.0, 13) -> EPiServer.CMS.UI.Core 12.1.0 -> EPiServer.CMS.AspNetCore.Templating [12.0.3, 13) => EPiServer.CMS.AspNetCore 12.0.3 => EPiServer.CMS.Core 12.0.3 -> EPiServer.Framework 12.0.3

Note the semi open version ranges [12.0.3, 13) meaning "any 12.x version higher than 12.0.3". On restore, Nuget will be processing these graphs from the top (the project). When it finds a version range, it will by default pick the lowest version in the range, and then move on from there. While traversing these dependency trees it can detect version conflicts between the different paths, but it will always stick to the version it already selected from a range. In the example above, all of the different versions are resolved in a compatible way, using the version 12.0.3 of the 1:1 version mapped packges in the "CMS Core" family (see appendix in the end of this article for more info).

The mix between the complex dependency graph and the 1:1 requirements is what creates issues with the Nuget algorithm. This is best illustrated with an example.

Example: Adding DXP support to plain CMS project

So the EPiServer.CMS package gives you what you need for a plain CMS site. But in the interest of modularity it does not automatically pull in the requirements to run on the DXP service (because you could be running elsewhere). To deploy your solution to the DXP service you need to add the EPiServer.CloudPlatform.Cms package.

Let's say you start with a plain project referencing EPiServer.CMS 12.1.0. This is an old version already, but I chose it becuase it illustrates one of the dependency resolution problems well. 

Now you want to be compatible with DXP, so you install EPiServer.CloudPlatform.Cms, and just pick the latest version 1.0.3.

Install EPiServer.CloudPlatform.Cms 1.0.3.

This fails! Why does it fail? Let's look at one specific path of the dependency graph of EPiServer.CloudPlatform.Cms:

EPiServer.CloudPlatform.Cms 1.0.3 -> EPiServer.CMS.AspNetCore [12.0.4, 13) -> EPiServer.CMS.Core 12.0.4 -> EPiServer.Framework 12.0.4

Compare this to just one of the paths of EPiServer.CMS 12.1.0 we looked at in the previous section: 

EPiServer.CMS 12.1.0 -> EPiServer.CMS.AspNetCore.HtmlHelpers [12.0.3, 13) -> EPiServer.CMS.AspNetCore.Mvc 12.0.3 -> EPiServer.CMS.AspNetCore.Routing 12.0.3 -> EPiServer.CMS.AspNetCore.Templating 12.0.3 -> EPiServer.CMS.AspNetCore 12.0.3 -> EPiServer.CMS.Core 12.0.3 -> EPiServer.Framework 12.0.3

As you can see from the first dependency (EPiServer.CMS 12.1.0 -> EPiServer.CMS.AspNetCore.HtmlHelpers [12.0.3, 13)), we're fully compatible with the 12.0.4 version of EPiServer.CMS.AspNetCore.HtmlHelpers (becuase it accepts a range from 12.0.3 to 13) and all the other 12.0.4 packages. But since nuget picks the lowest version in the range and sticks to it, it ends up with a conflict on several packages, including EPiServer.Framework that it mentions (EPiServer.CMS path wants 12.0.3, EPiServer.CloudPlatform.Cms path wants 12.0.4). 

At this stage you have to help. By adding a direct dependency to the project you can tiebreak this conflict, because of the "nearest wins" rule in the dependency resolution rules a direct project dependency will always win over tansitive dependencies.

Install EPiServer.Framework 12.0.4, then try again with EPiServer.CloudPlatform.Cms 1.0.3.

This still fails! But let's continue following the hints:

Install EPiServer.CMS.AspNetCore.Routing 12.0.4, then try again with EPiServer.CloudPlatform.Cms 1.0.3.

Success! But wait, even though the installation succeded, there are a couple of warnings.

warning NU1608: Detected package version outside of dependency constraint: EPiServer.Hosting 12.0.3 requires EPiServer.Framework (= 12.0.3) but version EPiServer.Framework 12.0.4 was resolved.
warning NU1608: Detected package version outside of dependency constraint: EPiServer.CMS.AspNetCore.Mvc 12.0.3 requires EPiServer.CMS.AspNetCore.Routing (= 12.0.3) but version EPiServer.CMS.AspNetCore.Routing 12.0.4 was resolved.

Let's look at a couple of the dependency paths again:

EPiServer.CMS 12.1.0 -> EPiServer.Hosting [12.0.3, 13) -> EPiServer.Framework 12.0.3
EPiServer.CMS 12.1.0 -> EPiServer.CMS.AspNetCore.HtmlHelpers [12.0.3, 13) -> EPiServer.CMS.AspNetCore.Mvc 12.0.3 -> EPiServer.CMS.AspNetCore.Routing 12.0.3 -> EPiServer.CMS.AspNetCore.Templating 12.0.3 -> EPiServer.CMS.AspNetCore 12.0.3 -> EPiServer.CMS.Core 12.0.3 -> EPiServer.Framework 12.0.3

So let's install the version of EPiServer.Hosting that depends on EPiServer.Framework 12.0.4, which by no coincidence is EPiServer.Hosting 12.0.4.

Install EPiServer.Hosting 12.0.4.

One warning down, one to go!

warning NU1608: Detected package version outside of dependency constraint: EPiServer.CMS.AspNetCore.Mvc 12.0.3 requires EPiServer.CMS.AspNetCore.Routing (= 12.0.3) but version EPiServer.CMS.AspNetCore.Routing 12.0.4 was resolved.
Install EPiServer.CMS.AspNetCore.Mvc 12.0.4.

All is good now, right?

warning NU1608: Detected package version outside of dependency constraint: EPiServer.CMS.AspNetCore.HtmlHelpers 12.0.3 requires EPiServer.CMS.AspNetCore.Mvc (= 12.0.3) but version EPiServer.CMS.AspNetCore.Mvc 12.0.4 was resolved.

Wait, what? There's a new warning? Ok, let's be persistent, install EPiServer.CMS.AspNetCore.HtmlHelpers 12.0.4 too:

Install EPiServer.CMS.AspNetCore.HtmlHelpers 12.0.4.

Success! No more warnings!

You now have this set of compatible packages:

<PackageReference Include="EPiServer.CloudPlatform.Cms" Version="1.0.3" />
<PackageReference Include="EPiServer.CMS" Version="12.1.0" />
<PackageReference Include="EPiServer.CMS.AspNetCore.HtmlHelpers" Version="12.0.4" />
<PackageReference Include="EPiServer.CMS.AspNetCore.Mvc" Version="12.0.4" />
<PackageReference Include="EPiServer.CMS.AspNetCore.Routing" Version="12.0.4" />
<PackageReference Include="EPiServer.Framework" Version="12.0.4" />
<PackageReference Include="EPiServer.Hosting" Version="12.0.4" />

Optionally, perhaps you have already figured out from the dependency chains you've seen (or from reading the appendix): Because EPiServer.CMS.AspNetCore.Mvc and EPiServer.CMS.AspNetCore.Routing are transitive dependencies of (and in 1:1 version relationship with) EPiServer.CMS.AspNetCore.HtmlHelpers, you may actually be able to remove these two dependencies to clean up a bit. Indeed that works, leaving you with this set of compatible packages:

<PackageReference Include="EPiServer.CloudPlatform.Cms" Version="1.0.3" />
<PackageReference Include="EPiServer.CMS" Version="12.1.0" />
<PackageReference Include="EPiServer.CMS.AspNetCore.HtmlHelpers" Version="12.0.4" />
<PackageReference Include="EPiServer.Framework" Version="12.0.4" />
<PackageReference Include="EPiServer.Hosting" Version="12.0.4" />

Updating with additional dependencies

When you used to update something like the umbrella package EPiServer.CMS in the old packages.config world, nuget would also update (overwrite) all its dependencies, e.g. EPiServer.Framework. This is not the case with PackageReference. While transitive dependencies are implicit, direct dependencies are taken very explicitly. Sadly this can cause problems when updating a higher level package like EPiServer.CMS. Let's start from the state in the step before, and try to update EPiServer.CMS to 12.3.2, the latest version at the time of writing.

Update EPiServer.CMS to 12.3.2

This fails, because EPiServer.CMS 12.3.2 requires 12.3.0 or higher of the CMS Core family packages. This is incompatible with the 12.0.4 versions you have now explicitly installed. The way this shows is it recommends adding EPiServer.CMS.AspNetCore.Templating 12.3.0, so let's try that:

Install EPiServer.CMS.AspNetCore.Templating 12.3.0

But that is in conflict with the currently installed EPiServer.Framework 12.0.4. So let's try to update EPiServer.Framework first.

Update EPiServer.Framework to 12.3.0, then add EPiServer.CMS.AspnetCore.Templating 12.3.0.

This succeeds but not unsurprisingly you get new warnings because we still have the older versions of EPiServer.Hosting and EPiServer.CMS.AspNetCore.HtmlHelpers expecting an older verison of EPiServer.Framework. So update those too:

Update EPiServer.Hosting and EPiServer.CMS.AspnetCore.HtmlHelpers to 12.3.0.

Now we can finally update EPiServer.CMS:

Update EPiServer.CMS to 12.3.2

You now have this set of compatible packages without warnings:

<PackageReference Include="EPiServer.CloudPlatform.Cms" Version="1.0.3" />
<PackageReference Include="EPiServer.CMS" Version="12.3.2" />
<PackageReference Include="EPiServer.CMS.AspNetCore.HtmlHelpers" Version="12.3.0" />
<PackageReference Include="EPiServer.CMS.AspNetCore.Templating" Version="12.3.0" />
<PackageReference Include="EPiServer.Framework" Version="12.3.0" />
<PackageReference Include="EPiServer.Hosting" Version="12.3.0" /> 

But that update of EPiServer.CMS was a bit messy. Let's take a step back and think about where it all started. It was EPiServer.CloudPlatform.Cms that needed version 1.0.4 of EPiServer.Framework. But now you have 12.3.0. And EPiServer.CMS 12.3.2 has EPiServer.Framework 12.3.0 as a transitive dependency. So what if you just try to remove extra packages you added, leaving only EPiServer.CMS and EPiServer.CloudPlatform.Cms. Would it still work? Indeed it does! Here's a minimal set of compatible packages:

<PackageReference Include="EPiServer.CloudPlatform.Cms" Version="1.0.3" />
<PackageReference Include="EPiServer.CMS" Version="12.3.2" />

Updating specific packages

Ok, so let's again use the previous end state as a starting point for the next scenario. We know EPiServer.CMS 12.3.2 implicitly gives us version 12.3.0 of the CMS Core packages. At the time of writing the newest version of for example EPiServer.CMS.Core is 12.4.1. What if that contains a bugfix you want? You then have two options. Either you wait until there is a EPiServer.CMS package that has EPiServer.CMS.Core 12.4.1 (or higher) as a dependency. It will eventually get there, but it might take some time.

Or you reference the package you want directly. Let's try that, install EPiServer.CMS.Core 12.4.1. It fails, once again with an error similar to the one you saw in the beginning:

So you could go through the same steps again, starting by adding EPiServer.Framework and walking through errors and warnings. Or you shortcut this based on what you learned earlier.

You ended up having to reference EPiServer.Framework, EPiServer.CMS.AspNetCore.HtmlHelpers and EPiServer.Hosting. Why those specifically? HtmlHelpers and Hosting because they are at top of the dependency chain from EPiServer.CMS, where Nuget would pick the lowest compatible version. Framework because it is at the bottom where different resolution paths could come to conflicing resolutions, and by bringing it up to a top level dependency you act as a tiebreaker. So what about adding version 12.4.1 of those instead? If you do it manually in the csproj you can just add them all at once. If you do it using the tools, you have to preemptively break the tie, and install EPiServer.Framework first:

Install EPiServer.Framework 12.4.1, EPiServer.Hosting 12.4.1 and EPiServer.CMS.AspNetCore.HtmlHelpers 12.4.1.

You now have this set of compatible packages:

<PackageReference Include="EPiServer.CloudPlatform.Cms" Version="1.0.3" />
<PackageReference Include="EPiServer.CMS" Version="12.3.2" />
<PackageReference Include="EPiServer.CMS.AspNetCore.HtmlHelpers" Version="12.4.1" />
<PackageReference Include="EPiServer.Framework" Version="12.4.1" />
<PackageReference Include="EPiServer.Hosting" Version="12.4.1" />

More packages or projects, more problems, but same solutions

If you add Commerce (or Forms, or...) you risk running into more issues like this, especially if you are trying to stay current on the main packages' depencendies like in the last example. But to solve those issues the method is the same. Follow the hints in errors and warnings and work your way through. Look at the dependencies of packages and you can work out how to simplify the set of packages you reference directly. We will look into if we can simplify this (without sacrificing something more valuable) but at least now you know in principle how to work around issues you run in to.

Similarily, if you have a multi-project solution, your lower layer projects will work the same as packages when your higher level projects are built, i.e. the nearest wins rule will take those projects into account. This may produce slightly different results than if you have only one web project, but in principle is the same. The key thing to remember is to keep the same / compatible dependency versions across the projects.

Appendix: Package "families"

This section describes packages that are versioned together and in most cases are dependent 1:1 (at the time of writing, there may be future changes). You can use this as a reference to know which packages to update to resolve a conflict in versions on one or more of the packages. If you have PackageReference elements to several packages in the same family, you can just manually update all of them at once to reference the same version.

You may even want to declare a PropertyGroup with a named property holding the version, and reference this in multiple places in your csproj. If you have a multi-project solution you can extract the PropertyGroup to a separate file (convention is to use the ".props" suffix) and use the Import element in each of your projects to have access to the parameter across dependencies and have a consistent dependency version throughout.

The CMS Core family

EPiServer.CMS.AspNetCore.*
EPiServer.CMS.Core
EPiServer.Framework
EPiServer.Framework.AspNetCore
EPiServer.Hosting

The CMS UI family

EPiServer.CMS.UI*

The Commerce family

EPiServer.Commerce.Core
EPiServer.Commerce.UI*

The Find family

EPiServer.Find*
(Except EPiServer.Find.Commerce)

The Content API family

EPiServer.ContentDeliveryApi.*
EPiServer.ContentManagementApi
(Note: Not all packages in this family are updated to .NET 5)

The Product Recs family

EPiServer.Tracking.Commerce
EPiServer.Personalization.Commerce

Mar 15, 2022

Comments

Antti Alasvuo
Antti Alasvuo Mar 15, 2022 10:46 AM

I wanted to give ten stars **********, thank you Magnus for the excellent study on the dependencies.

Magnus Rahl
Magnus Rahl Mar 15, 2022 12:56 PM

❤️🙏 Antti, I'm so happy you found it useful (and got through the wall of text)

Vincent
Vincent Mar 15, 2022 11:33 PM

Absolutely agree with Antti. It was an exceptionally well-written blog post on dependencies subject.

I recently encountered dependency conflict issues a few times and it caused my head spin a lot. I have completed reading this entire article and it clears many of questions in my head, especially the Appendix helps a lot.  Thank you Magnus.

Please login to comment.
Latest blogs
How to use CacheTagHelper with content areas in Optimizely CMS 12

I might be going out on a limb here - if you have a better solution, feel very free to share it!  Upgrading your Optimizely web application from .N...

Andreas J | Dec 2, 2022

The 1001st Piece in your 1000 Piece Puzzle: .NET Default Interface Functions

I was recently working with a client who wanted a reasonably large subsystem added to Optimizely that would add automated management to their...

Greg J | Nov 28, 2022 | Syndicated blog

Video Demonstration, creating a CMS12 Alloy Sample Site

Hey All Below you will find a quick video demonstration on how to install a local version of Alloy Sample based on CMS12 / .Net 6. As you will see ...

Minesh Shah (Netcel) | Nov 28, 2022

How to create an admin user I Optimizely CMS – with Episerver CLI

In this blog post I’ll show how to create an admin user for Optimizely CMS in a new environment where you don’t have access to the admin interface.

Ove Lartelius | Nov 28, 2022 | Syndicated blog