Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Supporting both LTS and Current releases for ASP.NET Core

$
0
0

Some time ago, I wrote a post on how to use custom middleware to set various security headers in an ASP.NET Core application. This formed the basis for a small package on GitHub and NuGet that does just that, it adds standard headers to your responses like X-Frame-Options and X-XSS-Protection.

I recently updated the package to include the Referrer-Policy header, after seeing Scott Helme's great post on it. When I was doing so, I was reminded of a Pull Request made some time ago to the repo, that I had completely forgotten about (oops 😳):

Update .NET Core packages to 1.1 #16

As you can see, this PR was upgrading the packages used in the package to the 'Current' Release of ASP.NET Core at the time. Discovering this again got me thinking about the new versioning approach to .NET Core, and how to support both versions of the framework as a library author.

The two tracks of .NET Core

.NET Core (and hence, ASP.NET Core) currently has two different release cadences. On the one hand, there is the Long Term Support (LTS) branch, which has a slow release cycle, and will only see bug fixes over its lifetime, no extra features. Only when a new (major) version of .NET Core ships will you see new features. The plus sides to using LTS are that it will be the most stable, and is supported by Microsoft for three years.

On the other hand, there is the Current branch, which is updated at a much faster cadence. This branch does see features added with subsequent releases, but you have to make sure you keep up with the releases to remain supported. Each release is only supported for 3 months once the next version is released, so you have to be sure to update your apps in a timely fashion.

You can think of the LTS branch as a sub-set of the Current branch, though this is not strictly true as patch releases are made to fix bugs in the LTS branch. So for the (hypothetical) Current branch releases:

  • 1.0.0 - First LTS release
  • 1.1.0
  • 1.1.1
  • 1.2.0
  • 2.0.0 - Second LTS release
  • 2.1.0

only the major versions will be be LTS releases.

Package versioning

One of the complexities introduced by adopting the more modular approach to development taken in .NET Core, where everything is delivered as individual packages, is the fact the individual libraries that go into a .NET Core release don't necessarily have the same package version as the release version.

I looked at this in a post about a patch release to the LTS branch (version 1.0.3). The upshot is that the actual packages that go into a release could be a variety of different values. For example, in the 1.0.3 release, the following packages were all current:

"Microsoft.ApplicationInsights.AspNetCore" : "1.0.2",
"Microsoft.AspNet.Identity.AspNetCoreCompat" : "0.1.1",
"Microsoft.AspNet.WebApi.Client" : "5.2.2",
"Microsoft.AspNetCore.Antiforgery" : "1.0.2",
"Microsoft.Extensions.SecretManager.Tools" : "1.0.0-preview4-final",
"Microsoft.Extensions.WebEncoders" : "1.0.1",
"Microsoft.IdentityModel.Protocols.OpenIdConnect" : "2.0.0",

It's clear that versioning is a complex beast...

Picking package versions for a library

With this in mind, I was faced with deciding whether to upgrade the package versions of the various ASP.NET Core packages that the security headers library depends on. Specifically, these were originally:

"Microsoft.Extensions.Options": "1.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0",
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0"

The library itself uses some of the ASP.NET Core abstractions around dependency injection and IOption, hence the dependencies on these libraries. However the version of the packages it was using were all 1.0.0. These all correspond to the first release on the LTS branch. The question was whether to upgrade these packages to a newer LTS version, to upgrade them to the latest Current branch package versions, or to just leave them as they were.

To be clear, the library itself does not depend on anything that is specific to any particular package version; it is using the types defined in the first LTS release and nothing from later releases.

The previous pull request I mentioned was to update the packages to match those on the Current release branch. My hesitation with doing so is that this could cause problems for users who are currently sticking to the LTS release branch, as I'll explain shortly.

NuGet dependency resolution

The problem all stems from the way NuGet resolves dependencies for packages, where different versions of a package are referenced by others. This is a complex problem, and there are some great docs covering it on the website which are well worth a read, but I'll try and explain the basic problem here.

Imagine you have two packages that provide you some middleware, say my SecurityHeadersMiddleware package, and the HttpCacheHeaders package (check it out on GitHub!). Both of these packages depend on the Microsoft.AspNetCore.Http.Abstractions package. Just considering these packages and your application, the dependency chain looks something like the following:

Dependencies for 3 packages

Now, if both of the middleware packages depend on the same LTS version of Microsoft.AspNetCore.Http.Abstractions then there is no problem, NuGet knows which version to restore and everything is great. In reality though, the chances of that are relatively slim.

So what happens if I have updated the SecurityHeadersMiddleware package to depend on the Current release branch, say version 1.1.0? This is where the different NuGet rules kick in. (Honestly, check out the docs!)

Package dependencies are normally specified as a minimum version, so I would be saying I need at least version 1.1.0. NuGet tries to satisfy all the requirements, so if one package requires at least 1.0.0 and another requires at least 1.1.0, then it knows it can use the 1.1.0 package to satisfy all the requirements.

However, it's not quite as simple as that. NuGet uses a rule whereby the first package to specify a version for a package wins. So the package 'closest' to your application will 'win' when it comes to picking which version of a package is installed.

For example, in the version below, even though the HTTP Cache Headers package specifies a higher minimum version of Microsoft.AspNetCore.Http.Abstractions than in the SecurityHeadersMiddleware, the lower version or 1.0.0 will be chosen, as it is further to the left in the graph.

choosing incorrect version

This behaviour can obviously cause problems, as it means packages could end up using an older version of a package than they specify as a dependency! Obviously it can also end up using a newer version of a package than it might expect. This theoretically should not be a problem, but in some cases it can cause issues.

Handling dependency graphs is tricky stuff…

Implications for users

So all this leads me back to my initial question - should I upgrade the package versions of the NetEscapades.AspNetCore.SecurityHeaders package? What implications would that have for people's code?

An important point to be aware of when using the Microsoft ASP.NET Core packages is that you must use all your packages on the same version - all LTS or all Current.

If I upgraded the package to use Current branch packages, and you used it in a project on the LTS branch, then the NuGet graph resolution rules could mean that you ended up using Current release version of the packages I referenced. That is not supported and could result in weird bugs.

For that reason, I decided to stay on the LTS packages. Now, having said that, if you use the package in a Current release project, it could technically be possible for this to result in a downgrade of the packages I reference. Also not good…

Luckily, if you get a downgrade, then you will be warned with a warning/error when you do a dotnet restore. You can easily fix this by adding an explicit reference to the offending package in your project. For example, if you had a warning about a downgrade from 1.1.0 to 1.0.0 with the Microsoft.AspNetCore.Http.Abstractions package, you could update your dependencies to include it explicitly:

{
    "NetEscapades.AspNetCore.SecurityHeaders" : "0.3.0", //<- depends on v1.0.0 ... 
    "Microsoft.AspNetCore.Http.Abstractions"  : "1.1.0"  //<- but v1.1.0 will win 
}

The explicit reference puts the dependent package further left on the dependency graph, and so that will be preferentially selected - version 1.1.0 will be installed even though NetEscapades.AspNetCore.SecurityHeaders depends on version 1.0.0.

Creating multiple versions

So does this approach make sense? For simple packages like my middleware, I think so. I don't need any of the features from later releases and it seems the easiest approach to manage.

Another obvious alternative would be to keep two concurrent versions of the package, one for the LTS branch, and another for the Current branch. After all, that's what happens with the actual packages that make up ASP.NET Core itself. I could have a version 1.0.0 for the LTS stream, and a version 1.1.0 for the Current stream.

The problem with that in my eyes, is that you are combining two separate streams - which are logically distinct - into a single version stream. It's not obvious that they are distinct, and things like the GUI for NuGet package manager in Visual Studio would not know they are distinct, so would always be prompting you to upgrade the LTS version packages.

Another alternative which fixes this might be to have two separate packages, say NetEscapades.AspNetCore.SecurityHeaders.LTS and NetEscapades.AspNetCore.SecurityHeaders.Current. That would play nicer in terms of keeping the streams separate, but just adds such an overhead to managing and releasing the project, that it doesn't seem worth the hassle.

Conclusion

So to surmise, I think I'm going to stick with targeting the LTS version of packages in any libraries on GitHub, but I'd be interested to hear what other people think. Different maintainers seem to be taking different tacks, so I'm not sure there's an obvious best practice yet. If there is, and I've just missed it, do let me know!


Viewing all articles
Browse latest Browse all 743

Trending Articles