Using NuGet with packages.lock.json

When I was looking into GitHub Actions, I found an example configuration for dependency caching that seemed wrong to me:

- uses: actions/cache@v3
    path: ~/.nuget/packages
    # Look to see if there is a cache hit for the corresponding requirements file
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
    restore-keys: |
      ${{ runner.os }}-nuget

If you are not familiar with this feature, the configuration above specifies that a unique key for caching dependencies should be generated by hashing all files named package.lock.json in the repository. For this to make sense, the referenced NuGet packages should be listed in these files.

I would expect the *.csproj files to be used instead. That's where the referenced NuGet packages are specified. Not in a file suspiciously similar in name to npm's packages-lock.json.

Although this is the case by default for .NET projects, you can get NuGet to generate a package.lock.json file for your .NET project by adding the following to its *.csproj file:


This enables the repeatable package restore feature for NuGet. It generates the package.lock.json file for the project, which has some similarities to npm's packages-lock.json file:

  • It contains not only direct (explicitly specified) dependencies, but also transitive dependencies required by the referenced dependencies.
  • It lists all dependencies for each dependency.
  • It contains a hash for each dependency to ensure the same content.

The file is updated every time you add, remove, or modify a dependency, and also when you restore packages if the packages could not be resolved using the existing lock file. If you want to prevent the latter and have the restore fail instead (which makes sense for CI/CD builds), you should add the --locked-mode option to the restore command:

dotnet restore --locked-mode

So what does this mean for GitHub Actions workflows? If you use the default configuration from above and do not have package.lock.json files in your repositories, the cache will not work as expected because it always uses the same cache key. You can find this out in the build log:

Cache saved with key: Linux-nuget-

This will result in only a single cache entry being created on the first build, which will not be updated even if the dependencies change afterwards because there is already a cache entry with that key.

To fix the problem, you have two options:

  1. Change the configuration to generate the hash from the *.csproj files instead:

    - uses: actions/cache@v3
        path: ~/.nuget/packages
        key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj*') }}
        restore-keys: |
          ${{ runner.os }}-nuget
  2. Enable repeatable package restore and commit up-to-date package.lock.json files in your repository.

In both cases, the generated cache key now contains a hash that changes when dependencies change, so a new cache entry is added afterwards:

Cache saved with key: Linux-nuget-13b498ad1d653a22316dd3a6d18c164ac46cb587388490371e2c1828d64452f0

If you choose the second way and enable repeatable package restore, you should also use --locked-mode when restoring packages to take advantage of this and have the build fail if dependencies cannot be restored in a repeatable way:

error NU1004: The package reference McMaster.Extensions.Hosting.CommandLine version has changed from [4.0.1, ) to [4.0.0, ).The packages lock file is inconsistent with the project dependencies so restore can't be run in locked mode. Disable the RestoreLockedMode MSBuild property or pass an explicit --force-evaluate option to run restore to update the lock file.

You can read about the cases in which this can happen in the original announcement of the feature.

A project with repeatable package restore and the corresponding GitHub Actions workflow can be found in my GitHub repository.

The GitHub Actions documentation and starter workflows for .NET use package.lock.json files from the repository to create a key for caching (NuGet) dependencies. This configuration will only work correctly if you have enabled repeatable package restore for your projects. If you have not, you should change the configuration to hash the *.csproj files instead.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Creative Commons License