Automated NuGet package version range updates in .NET projects using Renovate
Tags: dotnet, csharp, renovate
Published: 2024-04-12 13:30:00

In [my previous post about how to locally test and validate Renovate configuration files](https://anthonysimmon.com/locally-test-validate-renovate-config-files/), we saw how Renovate can be helpful in keeping our dependencies up-to-date. It recommended updates for the `Microsoft.Extensions.Hosting` package from `7.0.0` to `7.0.1` (minor) or `8.0.0` (major).

By default, the way Renovate handles NuGet package updates in .NET projects is suitable for the majority of cases. According to [the Renovate NuGet manager documentation](https://docs.renovatebot.com/modules/manager/nuget/) (a manager is a module that manages updates for a type of dependency), the following files are analyzed:

- Project files `.csproj`, `.fsproj`, and `.vbproj`,
- MSBuild files `.props` and `.targets`, including `Directory.Build.props`, `Directory.Build.targets`, and `Directory.Packages.props`,
- .NET tool installation manifests `dotnet-tools.json`,
- .NET SDK version definition files `global.json`.

Any reference to a NuGet package in these files will be analyzed by Renovate, and if a new version is available, a pull request will potentially be created to update it.

What is not specified in the Renovate NuGet manager documentation page is that it can only process packages referenced with the basic package version notation, for example:

```xml
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
```
However, specifying a package version in a .NET project can be much more advanced than that. Indeed, it's possible to [specify a version range](https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#version-ranges):

| Notation | Applied rule | Description |
|----------|--------------|-------------|
| 1.0 | x ≥ 1.0 | Minimum version, inclusive |
| [1.0,) | x ≥ 1.0 | Minimum version, inclusive |
| (1.0,) | x > 1.0 | Minimum version, exclusive |
| [1.0] | x == 1.0 | Exact version match |
| (,1.0] | x ≤ 1.0 | Maximum version, inclusive |
| (,1.0) | x < 1.0 | Maximum version, exclusive |
| [1.0,2.0] | 1.0 ≤ x ≤ 2.0 | Exact range, inclusive |
| (1.0,2.0) | 1.0 < x < 2.0 | Exact range, exclusive |
| [1.0,2.0) | 1.0 ≤ x < 2.0 | Mixed inclusive minimum and exclusive maximum version |

Take, for example, the library [OpenTelemetry.Instrumentation.Hangfire](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/Instrumentation.Hangfire-1.6.0-beta.1/src/OpenTelemetry.Instrumentation.Hangfire/OpenTelemetry.Instrumentation.Hangfire.csproj#L10), which imports the `Hangfire.Core` library:

```xml
<PackageReference Include="Hangfire.Core" Version="[1.7.0,1.9.0)" />
```

Unfortunately, [Renovate does not support NuGet version ranges by default](https://docs.renovatebot.com/modules/versioning/#nuget-versioning). However, [it is possible to extend its capabilities with regular expressions](https://docs.renovatebot.com/modules/manager/regex/).

It's required to extract the dependency name and the current version. In my previous example, the dependency name is `Hangfire.Core`, and I will consider the current version to be the left side of the range, which is `1.7.0`. You are free to decide which part of the range you wish to update.

Here's the regular expression that allows extracting the dependency name and the current version:

```text
<PackageReference\s+Include="(?<depName>[^"]+)"\s+Version="\[(?<currentValue>[^,]+),.*"
```

`depName` and `currentValue` are mandatory group names that can be interpreted by Renovate to perform the update.

Let's update our Renovate file to include the custom regex manager with our regular expression. I've omitted any content from the file that's not relevant for this article.

```json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:best-practices"
  ],
  "enabledManagers": [
    "nuget",
    "custom.regex"
  ],
  "customManagers": [
    {
      "customType": "regex",
      "fileMatch": [
        "\\.csproj$"
      ],
      "matchStrings": [
        "<PackageReference\\s+Include=\"(?<depName>[^\"]+)\"\\s+Version=\"\\[(?<currentValue>[^,]+),.*\""
      ],
      "datasourceTemplate": "nuget",
      "versioningTemplate": "nuget"
    }
  ]
}
```

If we execute Renovate locally with our new configuration, we should see that Renovate is capable of identifying the `1.7.0` version of `Hangfire.Core` and proposes an update:

```
DEBUG: packageFiles with updates (repository=local)
       "config": {
         "regex": [
           {
             "deps": [
               {
                 "depName": "Hangfire.Core",
                 "currentValue": "1.7.0",
                 "datasource": "nuget",
                 "versioning": "nuget",
                 "replaceString": "<PackageReference Include=\"Hangfire.Core\" Version=\"[1.7.0,1.9.0)\"",
                 "updates": [
                   {
                     "bucket": "non-major",
                     "newVersion": "1.8.11",
                     "newValue": "1.8.11",
                     "releaseTimestamp": "2024-02-23T11:56:41.437Z",
                     "newMajor": 1,
                     "newMinor": 8,
                     "updateType": "minor",
                     "branchName": "renovate/hangfire-monorepo"
                   }
                 ],
                 "packageName": "Hangfire.Core",
                 "warnings": [],
                 "sourceUrl": "https://github.com/HangfireIO/Hangfire",
                 "registryUrl": "https://api.nuget.org/v3/index.json",
                 "homepage": "https://www.hangfire.io/",
                 "currentVersion": "1.7.0",
                 "isSingleVersion": true,
                 "fixedVersion": "1.7.0"
               }
             ],
             "matchStrings": [
               "<PackageReference\\s+Include=\"(?<depName>[^\"]+)\"\\s+Version=\"\\[(?<currentValue>[^,]+),.*\""
             ],
             "datasourceTemplate": "nuget",
             "versioningTemplate": "nuget",
             "packageFile": "Project.csproj"
           }
         ]
       }
```

The Renovate Regex manager has many other parameters, so be sure to check out the documentation.

## References

- [NuGet package versioning](https://learn.microsoft.com/en-us/nuget/concepts/package-versioning)
- [Custom Renovate manager support using regexex](https://docs.renovatebot.com/modules/manager/regex/)
- [Renovate versioning schemes](https://docs.renovatebot.com/modules/versioning/)