Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] .NET8 support (as well as previous versions) is probably broken starting from v2 #377

Open
andriibratanin opened this issue Feb 4, 2025 · 34 comments
Assignees

Comments

@andriibratanin
Copy link

andriibratanin commented Feb 4, 2025

I stumbled upon NU1605 error (package downgrade detected) in my project (which targets .NET8 SDK) after installing FusionCache v2.

Checking https://github.com/ZiggyCreatures/FusionCache/blob/main/src/ZiggyCreatures.FusionCache/ZiggyCreatures.FusionCache.csproj:

<TargetFrameworks>netstandard2.0;netcoreapp3.1;net6.0;net7.0;net8.0</TargetFrameworks>

but finding also:

<ItemGroup>
	<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
</ItemGroup>

Reason:
Version 9 of "Microsoft.Extensions.Caching.Memory" NuGet package (as well as others) is causing NU1603 error in projects targeting .NET8 SDK (and probably 7, 6, etc.).

Arguments:

  • it is probably not right to reference NuGet packages of .NET9 while declaring support only for .NET8 max.
  • OR we are missing conditional ItemGroup in csproj files, like:
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
	<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
	<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <!-- not sure if this one is needed -->
</ItemGroup>
@jodydonetti
Copy link
Collaborator

Hi @andriibratanin , the version of Microsoft packages is not (necessarily) related to the corresponding .NET version. You can use a main package v9 with .NET 8 or 7.

For example: the package Microsoft.Extensions.Caching.Memory you mentioned is not tied to .NET 9, it can be used on .NET 8, 7, 6 and even earlier versions down to the old .NET Framework 4.6.2, see here:

Image

Can you give me more info about your dependencies?

@andriibratanin
Copy link
Author

andriibratanin commented Feb 5, 2025

I agree that versioning discussions are very old: dotnet/extensions#2700 (comment), but I believe the major version should not change

Here is my csproj file header:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>disable</Nullable>
    </PropertyGroup>

Here are my dependencies:

        <!--
        .NET Core
        -->
        <!-- https://github.com/dotnet/runtime/releases -->
        <!-- https://github.com/dotnet/extensions/releases -->
        <PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
        <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
        <PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
        <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
        <PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
        <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
        <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
        <PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
        <PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
        <PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
        <PackageVersion Include="Microsoft.Net.Http.Headers" Version="8.0.10" />

Please note I use CPM (central package management), so the notation is a little bit different (PackageVersion tag is used instead of PackageReference)

NU1605 errors are not trivial to investigate. For example, the original error message was:

Error NU1605 : Warning As Error: Detected package downgrade: Microsoft.Extensions.DependencyInjection.Abstractions from 9.0.0 to 8.0.2. Reference the package directly from the project to select a different version. 
 CE.Caching -> Microsoft.Extensions.Caching.StackExchangeRedis 8.0.12 -> Microsoft.Extensions.Logging.Abstractions 9.0.0 -> Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0) 
 CE.Caching -> Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
1>------- Finished building project: CE.Caching. Succeeded: False. Errors: 1. Warnings: 0

but it has nothing to deal with Microsoft.Extensions.Caching.StackExchangeRedis:
Image
as the only package referencing .NET9 is FusionCache:
Image

We will be able to see the correct error message only after explicitly setting package versions:

        <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
        <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
        <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />

finally resulting in:

Error NU1605 : Warning As Error: Detected package downgrade: Microsoft.Extensions.Caching.Memory from 9.0.0 to 8.0.1. Reference the package directly from the project to select a different version. 
 CE.Caching -> ZiggyCreatures.FusionCache 2.1.0 -> Microsoft.Extensions.Caching.Memory (>= 9.0.0) 
 CE.Caching -> Microsoft.Extensions.Caching.Memory (>= 8.0.1)
1>------- Finished building project: CE.Caching. Succeeded: False. Errors: 1. Warnings: 0

which seems like a dead-end without "partially upgrading" to .NET9

PS: explicitly sticking .NET version with global.json like the following doesn't help:

{
  "sdk": {
    "allowPrerelease": false,
    "rollForward": "latestFeature",
    "version": "8.0.300"
  }
}

@scardenas
Copy link

scardenas commented Feb 5, 2025

This is happening to me too (same situation exactly). I can't upgrade to v2.

@jodydonetti
Copy link
Collaborator

I'm reading about this online to get a better grasp of the best practices (thanks for the links!)

So basically the idea would be to specify, for each TFM in the multi-target list, the package version related to that TFM? So if I have multi-target with .NET 8 and 9, I should specify the dependency on Microsoft.Extensions.Caching.Memory version 8 for .NET 8 and version 9 for .NET 9?

And what about .NET Standard 2.0, what should I do there?

Also, wouldn't this create confusion for users?

I'll take a look at existing Microsoft packages to see if they actually do the same.

I'm trying to understand how this would work with all the edge cases, so any help in exploring the scenarios is appreciated, thanks!

Regarding your specific: case I think you have a direct dependency on one of those packages with a specific version, for example you probably have a direct dependency on Microsoft.Extensions.Caching.Memory v8 and when updating to FusionCache v2 Nuget sees that it would require an upgrade to Microsoft.Extensions.Caching.Memory v9, but it doesn't do that because you have a direct dependency declared which you need to manually update first. Can you try update your Microsoft.Extensions.Caching.Memory dependency to v9 and try again to update to FusionCache v2, to see if that fixes it, and let me know? Thanks!

@jodydonetti
Copy link
Collaborator

I'll add: looking for example at the Microsoft.Extensions.Caching.Hybrid package, they ( @mgravell basically) are doing the same thing there:

Image

Another example is System.Text.Json, and they do the same:

Image

Maybe is the direct reference I mentioned? Or maybe something specific about CPM (central package management)?

I'll keep looking into this.

@andriibratanin
Copy link
Author

andriibratanin commented Feb 5, 2025

Regarding your specific: case I think you have a direct dependency on one of those packages with a specific version

No, I have no dependency on Microsoft.Extensions.Caching.Memory. In my case NU1605 error is related to Microsoft.Extensions.DependencyInjection.Abstractions package which I have to reference directly due to code requirements.


Let me help you to figure how it works

Just create an empty console application or worker service targeting .NET8 and then experiment with installing the following sets of packages (no code changes are required at all):

Case 1: Only FusionCache is installed:

        <!-- Case 1: Only FusionCache is installed - builds ok -->
        <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>
        <PackageReference Include="ZiggyCreatures.FusionCache" Version="2.1.0" />
        <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.1.0" />

this builds ok


Case 2: FusionCache + DependencyInjection.Abstrations:

        <!-- Case 2: FusionCache + DependencyInjection.Abstrations - build fails -->
        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
        <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>
        <PackageReference Include="ZiggyCreatures.FusionCache" Version="2.1.0" />
        <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.1.0" />

this will not build!


Now, interesting ones, my counter-arguments:

Case 3: MemoryCache v8 + DependencyInjection.Abstrations (no FusionCache)

       <!-- Case 3: MemoryCache v8 + DependencyInjection.Abstrations - builds ok -->
        <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
        <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>

this builds ok!


BUT, as soon as we upgrade the version of MemoryCache to 9 everything breaks:

Case 4: MemoryCache v9 + DependencyInjection.Abstrations:

        <!-- Case 4: MemoryCache v9 + DependencyInjection.Abstrations - build fails -->
        <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.1" />
        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
        <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>

build fails.

So, unfortunatelly, we are mistaken supposing that stated support of version below 9 is true when we are not actually on .NET9


Extensions packages are using Central Package Management with conditionals:

I suppose they run build multiple times (to publish packages for v8, v9, etc.)

@juantxorena
Copy link

juantxorena commented Feb 6, 2025

I'm also hit by this so I cannot upgrade, since we are in the LTS version. I don't really know about packaging and versioning, but as an example, another package I'm using (Grpc.Net.Client) seems to depend on the minimum needed version they need, instead of the latest:

Image

Here is the csproj file. There is this MicrosoftExtensionsLtsPackageVersion thing which I don't know what is it, but it seems relevant:

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="$(MicrosoftExtensionsLtsPackageVersion)" />
  </ItemGroup>

That variable is defined in a props file.

@andriibratanin
Copy link
Author

Just for reference: here is one more issue reported in Extensions repo regarding v9 packages usage in v8 applications:
dotnet/extensions#5644

@jodydonetti
Copy link
Collaborator

Hi all, thanks a lot for all the infos!

I'm trying to understand the best course of action to ease the update for you, without introducing some breaking changes all the others who already updated to v2.

Currently I'm thinking about a couple of options, like:

  • downgrading all the package refs to the v8 versions, since it was already like that in v1.4.1 when nobody mentioned any problem
  • downgrading all the package refs to an even lower version, since an higher one in the host project or in another package in the same host project would bump automatically anyway
  • specify different versions for the refs based on each TFM (so targeting v8 versions for .NET 8, v6 versions for .NET 6, etc)

Currently though I'm not sure which one (or another one) would be the best path.

I'll keep you updated, thanks again for the discussion and the sharing of info.

@jodydonetti
Copy link
Collaborator

Meanwhile, I'm trying to see if the .NET community can come up with some help:

Let's see if someone smarter than me can help 🤞

@alexaka1
Copy link

alexaka1 commented Feb 7, 2025

Can OP try to upgrade all direct references of Microsoft.* packages to 9.*?
And if not, why not? Afaik you should be upgrading these across major version as well. Don't confuse the package major version with current latest .Net version. They are versioned like this for convenience only, there is actually no runtime blockers as evidenced by the supported frameworks table on nuget.org

Also, why are you locking to the .Net 8 SDK? The SDK itself should be the latest always. The oldest SDK you can need today is .Net 6, but that's because 9 dropped support for building 6. If you develop for .Net 8 today, you need two things. 9 SDK and 8 runtime. This is not relevant to the issue, I'm just curious what is the justification.

@juantxorena
Copy link

Can OP try to upgrade all direct references of Microsoft.* packages to 9.*? And if not, why not? Afaik you should be upgrading these across major version as well. Don't confuse the package major version with current latest .Net version. They are versioned like this for convenience only, there is actually no runtime blockers as evidenced by the supported frameworks table on nuget.org

If that is the case, why force depend on the latest version instead of the minimum version needed? If there are no runtime blockers, older versions should work.

In any case, I disagree. In my opinion, 9.* packages are for .NET 9, and 8.* packages are for .NET 8. Take this one, for example (although any other AspNetCore would do). Versions 8.0.11 and 9.0.0 were released 3 months ago. Versions 8.0.12 and 9.0.1 were both released 24 days ago, meaning that the 8.* branch is still maintained. If it was only convenience, they wouldn't have bothered with 8.0.12.

Also, why are you locking to the .Net 8 SDK? The SDK itself should be the latest always. The oldest SDK you can need today is .Net 6, but that's because 9 dropped support for building 6. If you develop for .Net 8 today, you need two things. 9 SDK and 8 runtime. This is not relevant to the issue, I'm just curious what is the justification.

.NET 8 is LTS, .NET 9 isn't. That is the justification in my case (and probably in most of the people who are still with .NET 8). As per above, .NET 8 is still actively supported (as expected in a LTS version), so I will keep using it until .NET 10 arrives.

@jodydonetti I have found some other packages which depends on the "correct" Microsoft packages versions:

Image

That is from NSwag.AspNetCore. Here are the csproj and the nuspec, as you can see, for Microsoft.Extensions.ApiDescription.Server it depends on a 6 version, I assume because that's the minimum valid, and for Microsoft.Extensions.FileProviders.Embedded it depends on the version of the .NET target, so 8.* for .NET 8 and 9.* for .NET 9.

@jodydonetti
Copy link
Collaborator

jodydonetti commented Feb 7, 2025

Hi @andriibratanin

In my case NU1605 error is related to Microsoft.Extensions.DependencyInjection.Abstractions package which I have to reference directly due to code requirements

Help me understand one detail please: your requirements are to "reference Microsoft.Extensions.DependencyInjection.Abstractions" or to "reference Microsoft.Extensions.DependencyInjection.Abstractions v8" ?

The thing I'm not understanding is why it's not possible to update those references to the related v9 versions, since they are all backward compatible and would not create any issue (at least to the best of my knowledge, in case that's not the case let me know).

As an additional info: I know some are using FusionCache v2 on the very old .NET Framework v4.X, and even they don't have any problem at all, so I'm trying to understand where exactly is the problem, so I may get to a solution.

@alexaka1
Copy link

alexaka1 commented Feb 7, 2025

For clarity on .NET major releases:

The quality of all releases are exactly the same, the only difference is the length of support.

https://dotnet.microsoft.com/en-us/platform/support/policy

@alexaka1
Copy link

alexaka1 commented Feb 7, 2025

As for package versioning, I think we should look at other community packages instead.
Serilog recently upgraded to .Net 9, even though they support net462.
Refit also targets 8.x packages on TFMs that don't include System.Text.Json
To my suprise AKKA is stuck at .Net 6, so if you use it now, you use the netstandard2.0 TFM variant, so you get System.Threading.Channels at a 6.0 version, but your project can still manually 'upgrade' to 9.0.0.
You would be hard pressed to argue that OpenTelemetry stopped supporting .Net 8. They also target .Net 9 even though they support as low as net462.
MediatR, net6 TFM, 8.x package
Polly does both. It requires 8.x for anything lower than net9.0. Which means when you target net6.0, you must upgrade to 8.x.
Same for MassTransit. Actually even more interesting I think this branch causes .netstand2.0 to use System.Text.Json:9.0.0. Meaning if you use .net6.0, tough luck, gotta upgrade to .Net 9, which is misleading. You don't upgrade your TFM, just the nuget version.

So it is actually quite common to depend on the latest major version of a package, even if you multi TFM to older versions.

@alexaka1
Copy link

alexaka1 commented Feb 7, 2025

As a counter point take NLog for example they tried to do what OP wishes, but it has a very unintended side effect of .net 9 falling back to netstandard2.0 and using the ancient Microsoft.Extensions.Logging:2.1.0 package on a project.

@snakefoot
Copy link

snakefoot commented Feb 7, 2025

As a counter point take NLog for example they tried to do what OP wishes, but it has a very unintended side effect of .net 9 falling back to netstandard2.0 and using the ancient Microsoft.Extensions.Logging:2.1.0 package on a project.

Believe .NET9 will fallback to closest target-framework which is NET8. Just like NET48-project will fallback to NET461.

When I compile a NET9-project that depend on NLog.Extensions.Logging then output-folder contains the NET8-assembly (and not NetStandard2)

@juantxorena
Copy link

As for package versioning, I think we should look at other community packages instead.

I think we should look at the ones I posted.

Serilog recently upgraded to .Net 9, even though they support net462.

Serilog replicates the .NET versioning schema exactly for these reason. Users of the .NET 8 should use the 8.* packages, as explained here.

You would be hard pressed to argue that OpenTelemetry stopped supporting .Net 8. They also target .Net 9 even though they support as low as net462.

Opentelemetry needs the latest versions of System.Diagnostics, but in any case, the decision of bumping all the dependencies to the latest version was also somewhat controversial

Polly does both. It requires 8.x for anything lower than net9.0. Which means when you target net6.0, you must upgrade to 8.x

Considering that .NET 8 is LTS, that's not as big of a problem. And in any case, they don't force STS packages to the LTS users.

For clarity on .NET major releases:

The quality of all releases are exactly the same, the only difference is the length of support.
https://dotnet.microsoft.com/en-us/platform/support/policy

That is irrelevant. Microsoft offers LTS versions for a reason. Some companies mandate the use of LTS.

In any case it seems that nuget guidances in this respect, so the library authors should depend on versions above the TFM ONLY if they need it, so this topic should be settled: do not force upgrades if it's not needed.

@alexaka1
Copy link

alexaka1 commented Feb 7, 2025

That last link seems to be the answer then. It is just Nuget hell. I personally had to make this decision once or twice too. I justified it by Microsoft saying the quality of releases is the same. Which I still stand by, the whole LTS thing is very silly, 3 years is not long term, (besides the point), but I have unfortunately encountered game breaking bugs that I reported to the core repo, and for each report the fix only arrived in the next major version. This was very dissappointing, so maybe that is my bias for not caring about LTS as to me it literally was meaningless since my bugs were left unfixed on the current version.

It does beg the next question though. What does this guidance mean to community libraries? Suddenly the support matrix for multi TFM libraries have increased quadratically. You now not only have to fight the runtime provided APIs, like Spanvariants of certain System.Text.Json methods, but also supporting 2-3 branches of nuget packages. Maybe you too depend on a library that doesn't follow this. They may have a different support policy and only fix security issues in the latest version. In which case you cannot adhere to this guidance. What is the guidance for netstandard2.0, or netframework? And I still don't understand if this is MSs position then why do they multi target the extensions libraries? I fear this is just going to discourage multi TFM libraries going forward, as the maintenance cost is increased significantly. Dotnet is supposed to make it easy to support older runtimes, this makes it harder imo.

Or the worst case scenario (I hope this isn't the case) this basically forces every single community library to adopt TFM specific support/versioning.

Also this is very interesting, the 8.x platform extensions were out of support last year already.

@jodydonetti
Copy link
Collaborator

This weekend I'll try to get to the bottom of this.

One thing to note about this specific case is that the HybridCache abstractions are there only in the v9 of the Microsoft.Extensions.Caching.Abstractions package, and they are needed in v2 because of this.

@bbehrens
Copy link

bbehrens commented Feb 8, 2025 via email

@andriibratanin
Copy link
Author

andriibratanin commented Feb 8, 2025

For me, "just upgrade" sounds strange, because it cancels the idea of versioning at all:

  • the major part of any version is meant to warn everyone of some incompatible changes, but now we are saying it is ok
  • DependencyInjection.Abstractions is the package you must reference when creating your own DLLs (you will have this dependency because of DI registrations and you cannot "hope" someone else already references it) - this is the reason of my "requirement by code"
  • it is non-sense to have a dependency forcing you to upgrade

Upgrades are actually not as simple as we try to imagine them - they take time: to make the decision, to read release notes, to re-compile. For example, do you know that "FluentAssertions" are no longer free starting from their version 8? "Just upgrade" policy may lead you to very unpleasant consequences one day...

Personally, it is not hard to check-out the repo and re-compile it targeting v8 - kudos to Jody and the community - the code is covered with tests very good. The fun fact is I am 99% sure it will compile and will not have any issues...

So, yes:

  • I think v8 packages are for .NET8
  • I am on LTS because it doesn't make sense to race for new "features" of SDK, it is just an instrument for building products and bringing value
  • I believe "the least version needed" policy for DLL is the good choice.

Happy have started this topic - we've already learned alot and, probably, gave another nudge to MS for clear guidances... ;)

@andriibratanin
Copy link
Author

andriibratanin commented Feb 8, 2025

Backporting patch for those who can't wait to play with such a beauty as v2:
https://gist.github.com/andriibratanin/3983fa5bec45e9aa8e640051d9bcac34

Only HybridCache was removed, Tags are still there.

@alexaka1
Copy link

alexaka1 commented Feb 8, 2025

  • the major part of any version is meant to warn everyone of some incompatible changes, but now we are saying it is ok

With all due respect @andriibratanin, you upgraded to a major version of FusionCache, and experienced an incompatible change that you disagree with.

And to reiterate it is NOT an incompatible change, because the package still works on net8.0 and lower. Why would the 9.x packages multi TFM net8.0 if they were meant for net9.0 only? I will continue to push back on the LTS vs STS stuff, because it is just silly and just a marketing gimmick. You are suggesting that LTS is more stable, which is just simply not true. It is just a gimmick, and Microsoft should get all the criticism for perpetuating this model.

@andriibratanin
Copy link
Author

andriibratanin commented Feb 8, 2025

With all due respect @andriibratanin, you upgraded to a major version of FusionCache, and experienced an incompatible change that you disagree with.

From the very first post:
"it is probably not right to reference NuGet packages of .NET9 while declaring support only for .NET8 max."

Why would the 9.x packages multi TFM net8.0 if they were meant for net9.0 only?

They are not multi-TFM, it is just a copy-paste in release notes. Please consider doing the experiment yourself from this post:
#377 (comment)

@jodydonetti
Copy link
Collaborator

jodydonetti commented Feb 8, 2025

Ok fellas, now that my weekly daily job + commute is off the table, I'm reading all the backlog of comments, links and whatnot, here and on social.

First thing that came up from a comment on Bluesky is this video, where at the 24m40s mark @DamianEdwards starts by saying "ok, this is an incredibly complicated question..."

Image

So yeah, will watch some videos, read some docs and csproj files posted here and will get back to you all with my thoughts.

Wish me luck.

@jodydonetti jodydonetti self-assigned this Feb 8, 2025
@alexaka1
Copy link

alexaka1 commented Feb 8, 2025

@andriibratanin

I did the experiment, this would be my solution:

Case 2 fixed:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <!-- Case 2: FusionCache + DependencyInjection.Abstrations - build fixed -->
-       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
+       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
        <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>
        <PackageReference Include="ZiggyCreatures.FusionCache" Version="2.1.0" />
        <PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.1.0" />
    </ItemGroup>
</Project>

Case 4 fixed:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <!-- Case 4: MemoryCache v9 + DependencyInjection.Abstrations - build fixed -->
        <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.1" />
-       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
+       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
        <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>
    </ItemGroup>
</Project>

This is what I meant by multi TFM.

@jodydonetti
Copy link
Collaborator

jodydonetti commented Feb 8, 2025

Ok so I read the things and watched the videos, thought about it both in general and in the particular case of FusionCache.

I'll now try to summarize the main points about this kerfuffle, step by step in a logical order, trying to stick to the things that are strictly related here to avoid confusion with secondary stuff.

Note

I may have missed something or made mistake in my analysis. In that case please let me know, ideally with some useful links to better understand the specific detail, thanks.

Here we go.

What we mean when we say ".NET"

Seemingly silly to specify this, but just to be all on the same page about the details and their nuance.

So .NET as a whole is a product, composed of multiple things, and the things that are relevant to this discussion are:

  • the .NET runtime (the thing that runs our .NET code)
  • the core packages (eg: Microsoft.Extensions.Caching.Abstractions)

Each .NET "main version" like 7, 8 and 9 corresponds NOT ONLY to the runtime, but ALSO to the related core packages that comes out with tha .NET product: for example, when .NET v9 came out, it was accompanied with v9 core packages.

This does NOT (necessarily) mean that each v9 core package requires v9 of the .NET runtime: since core packages are "just" packages, they are frequently backward compatible with older TFMs.

A TFM (Target Framework Moniker) is a string used to identify a version of .NET "product" (meaning, as a whole).
Somewhat nitpicking here, but not only a specific .NET product version (a.k.a: a ".NET release" like we see in November each year), but also a ".NET standard". What is a .NET standard? Like it's said in the official docs it is:

a formal specification of .NET APIs that are available on multiple .NET implementations. The motivation behind .NET Standard was to establish greater uniformity in the .NET ecosystem. .NET 5 and later versions adopt a different approach to establishing uniformity that eliminates the need for .NET Standard in most scenarios

Package versions

Now anyway, by targeting the .NET 8 TFM we basically get a dependency on all the related core packages from that version, but we can also reference those same packages (even in the same version) directly.

For example, System.Text.Json v9 does multi-targeting, meaning it targets multiple TFMs at the same time, including .NET Standard 2.0, which means it can also run on the very old .NET Framework 4.X from around 2018:

Image

As we can see when targeting .NET Standard 2.0 there are a lot of packages referenced, including System.IO.Pipelines v9 and System.Text.Encodings.Web v9, but when targeting .NET 8 these 2 are the only one there. Why? Because by targeting .NET 8 the other packages are already part of what .NET 8 is, just like when targeting .NET 9 nothing is there because these 2 packages have been added to the definition of what .NET 9 is, so it's not necessary anymore to directly reference them.

The main (but not necessarily the only) reason for multi-targeting a library is to have less dependencies declared in the manifest (even though they are there anyway at runtime because they are part of that runtime version itself).

Support Policy: STS/LTS

Each major .NET product version (so runtime + core packages) coming out in November each year has a corresponding support policy, either STS or LTS: an STS version is supported for 18 months, while an LTS is supported for 36 months (3 years).

For various reasons (eg: primarily costs associated with long term support of multiple versions) Microsoft decided that each major version of .NET would alternate between STS and LTS, so for example .NET 6 is LTS, .NET 7 is STS, .NET 8 is LTS and the recently released .NET 9 is STS.

Important

Now, this is important: the product quality, support quality , documentation and everything else for both an STS and an LTS version is exactly the same. Like totally, 100%. It just happens that the support duration on an STS is 18 months, which btw crosses over the next version (after 12 months) which will be LTS: this means that using an STS will give us 6 months after a new LTS version comes out to move to it.

One thing to pay attention to is that the support policy is for the entire product: so NOT ONLY for the .NET runtime, but also for the core packages.

This means that companies that, for whatever reason, only allows their tech people to use an LTS version, are forcing them to stay on an older tech stack: again, not just the runtime, but also core libs/packages, and so on. And you'd be surprised to know how many times companies don't even know that even by disallowing, for example, .NET 9 they are not in fact actually blocking all the v9 core packages, and they'll get updated anyway without nobody even noticing.

Package Version Lifting

Now let's say we create a console app for .NET 8: this means we are implicitly referencing the v8 core packages, right?
But what happens when we then add a reference to a package that, in turn, references a core package in v9?

An automatic "lift" happens, and now our console app is referencing v9 of that package, along with all its dependencies (probably v9 themselves).

The good @DamianEdwards clearly explains all of this in a video, where at the 24m40s mark starts with "ok, this is an incredibly complicated question...":

Image

I think his explanation is complex (because the thing itself is complex, and cannot simplified more) but masterfully clear in the way it's explained. Really, go watch it, it's that good.

So anyway, can the automatic lift always work? No, there's one case where this process breaks, and it's when our app has a direct dependency to the same package.

Basically, if for example in our app we directly reference v8 of System.Text.Json and then we also reference a Foo package that, in turn, references v9 of System.Text.Json, the system cannot auto-lift versions, because we are being explicit about the version of System.Text.Json we want (v8) but we are ALSO saying that we want to work with Foo which in turn requires System.Text.Json v9.

In cases like this we'll receive an error like "Detected package downgrade: System.Text.Json from 9.0.0 to 8.0.0" or similar.

So, what can we do? We need to either:

  • update the direct reference from v8 to v9
  • remove the direct reference and allow it to be taken from the other dependency
  • stop using Foo (or use an older version without the System.Text.Json v9 requirement)

So, what should library authors do?

Library authors: what versions to target?

Now, library authors like myself need to decide what to target: apart from multi-targeting or not, we need to pick a baseline minimum version.

Of course we can target whatever minimum version we want: let's keep in mind it's our own project, our choice, etc.

But.

But let's see what we may do to give good support to people and companies that (good or bad that it is, and I personally think it's at least very limiting) want to stick to LTS only versions.

While doing so, let's also say that a reasonable thing in this regard is to support only the last LTS (right now this means .NET 8): everything more than that (eg: also target .NET 6 or below) is of course possible, but let's say not strictly "necessary".

I can see 2 different options:

  • Last LTS: reference core packages with the same version as the last LTS (eg: now it would mean v8), either by targeting .NET Standard or a specific .NET version. This gives a reasonable support, and would last at least until a new LTS will come out
  • Multi-target with highest version per TFM: multi-target each desired TFM (eg: .NET 8, .NET 9, etc), including at least the latest TFM (reasonable for LTS-only companies, see above), and per each TFM have specific references to core packages for that TFM specific version. For example target .NET 8 and .NET 9 each with specific references to core packages v8 and v9. This gives an even broader support for users, but creates a targeting/reference matrix that can become quite hard to maintain, test, benchmark and in general support for the library author, which is a big burden. Also, what should the minimum package version be for something like .NET Standard 2.0?

Oh, and in the 2nd option above I said I "highest version" because instead of sticking to the baseline v8.0.0 of the LTS package, we can/should stick to the highest version (with the same major) since regarding the STS/LTS part it doesn't change anything, and because of things like CVEs like this:

Image

Anyway with both options users would still be able to directly upgrade core package references to v9 or more, and everything should (in theory) work correctly, so there's also freedom there.

But what if v9 core packages have new overloads that are more optimized? We library authors may have optimized code-paths via some #IF directives, but still that would enable the new stuff NOT when referencing v9 of the core packages, BUT ONLY when running on .NET v9, which is different, so a lot of potential good would be left on the table.

And what if instead v9 core packages have totally new stuff, with new types to use and maybe return from our public methods? It becomes harder to opt-in to those things via #IF directives.

Now, unless v9 core packages have some new features strictly needed by the library, "Last LTS" can be the reasonable thing to do, imho.

Ok so, can I do this for FusionCache v2?

Eh...

Ok so, FusionCache v2?

In theory I may easily change the references to v8 core packages for FusionCache, and in fact this is what I basically ended up doing in the past (meaning: generally sticking to the latest LTS), except... this time I can't, because of this feature.

You see, the HybridCache abstractions (eg: public abstract class HybridCache and related basic options classes) that 3rd parties like myself can implement, have been defined in v9 of the core Microsoft.Extensions.Caching.Abstractions package.

If I try to get back to v8 core packages the project would not compile, that's it.

The 2nd option above is also not an option, because by multi-targeting .NET 8 with v8 core packages and .NET 9 with v9 core packages, it would not compile for .NET 8 for the reasons above.

Sure, I can go with #IF directives and exclude "everything HybridCache" for .NET 8 and below, but... that would limit people targeting .NET 8 that have no problems at all updating to the latest core packages.

Case in point, currently I have about 27K downloads of v2+v2.1: even considering downloads for demos or little tests, we are talking about thousands of people already using it, including HIBP, Dometrain and others, happily using it as-is without any issue.

Another option I'm thinking about is that, in theory, I can stick to v8 core packages for the main package and create a new package that contains just the HybridCache adapter, where I can reference v9 packages. Want the HybridCache-compatible stuff? Use the extra package, but with v9 core packages. Don't want that? Use the main package only so that, if you want, you can stick to v8 core packages.

I even already have such package, see here:

Image

Before going GA with v2 though, the extra package became "useless" and I emptied+deprecated it, see here for why.

The problem in doing the extra package thing anyway is that this would be a breaking change, and everybody already happily using FusionCache v2 will then update and have problems.

On the other hand, I can simply stay like it is right now: user wanting the new stuff can update their core packages, or stay with the v1.4.1 version. The problem is that v2, apart from the HybridCache thing, contains A LOT of wonderful stuff like Tagging and Clear support, which are honestly huge.

And I'd really really like to give those features to the widest possible group of users 🥲

Oh, also: I'm working on adding support for the new (optional) IBufferDistributedCache inteface, to reduce allocations even more, but that is also defined in Microsoft.Extensions.Caching.Abstractions v9. This measn I'd have the same problem of not being able to reference it in the main package, if I go with the "v8-only in main package" route.

Conclusions (actually, not yet)

So anyway, I'm still thinking about what to do here, pondering pros and cons.

Meanwhile, please let me know what you think: did I miss something? Did I say something wrong? I'd like to know before making a decision.

Will update as soon as I get to a definitive answer.

Thanks!

PS: all of this will become basically useless as soon as .NET 10 (LTS) will be out in November. Yeah, I know, 9 months, but still.

@andriibratanin
Copy link
Author

@jodydonetti

I understand your points and priorities, so agree that it is better not to fix the "issue" in favor to move on and be first.

Personally, I've successfully re-built v2 without HybridCache for experiments - the link to the patch is above.

Now, without regard to FusionCache and without intent to offend anyone. Lessons learned for building DLLs:

  • never rely on things still not in production OR support them in a separate package (thank you for giving the idea!)
  • have a TFM support matrix
  • reference least needed major versions (still update minor to mitigate vulnerabilities)
    Once again, this ^ is subjective point of view.

Jody, I reassure you that no matter what your decision will be, our support and gratitude for your efforts will not become less.

Thank you very much for FusionCache!

@dariogriffo
Copy link

Why don't you use range version on your dependencies. Unless you are using any specifics of .net if you reference the dependency as Version="[8.0.0,)" should suffice. Then who install your package already decides the TFM.
If you want to make sure nothing will break just put 10 as non inclusive upper bound dependency and you are good with 9.

@jodydonetti
Copy link
Collaborator

jodydonetti commented Feb 9, 2025

Hi @dariogriffo

Why don't you use range version on your dependencies. Unless you are using any specifics of .net if you reference the dependency as Version="[8.0.0,)" should suffice

Well in general when you say Version="8.0.0" that alredy means "I'm ok with newer versions", so it's kinda the same thing, see here:

Image

Also nope, I can't specify 8 as the minimum version, because of this:

You see, the HybridCache abstractions (eg: public abstract class HybridCache and related basic options classes) that 3rd parties like myself can implement, have been defined in v9 of the core Microsoft.Extensions.Caching.Abstractions package.

If I try to get back to v8 core packages the project would not compile, that's it.

@jodydonetti
Copy link
Collaborator

jodydonetti commented Feb 9, 2025

Hi @andriibratanin

I understand your points and priorities, so agree that it is better not to fix the "issue"

Right now I have not settled for a definitive decision yet, I'm still thinking of a possible way out that would make everyone happy.
Can't promise I'll find a solution, but I'm still onto it 👍

in favor to move on and be first.

This is something I care to clarify.
I see your point, and I agree it would've made sense for some OSS author to rush a little bit to "be first", because of visibility, recognition, blah blah.

But again I really really care to say that this is NOT what I did: you are probably referring to the fact that "HybridCache is not out yet" and I came out first, but this is not what I did in this context.

Let me explain.

The "default HybridCache implementation from Microsoft" is not out yet, true, but FusionCache doesn't need that. FusionCache needs the "HybridCache abstractions" (aka: the abstract class and support options) and those came out officially as GA with .NET 9 in November, they are fixed in stone and cannot change, at all, at least until .NET 10. The only thing they may change in an hypothetical .NET 9.1 is some xml docs and stuff like that, which would not affect FusionCache at all.

I could have rushed to "be first" by coming out immediately in November after the .NET 9 release since all the pieces were basically already there, BUT I consciously decided to instead wait (risking not to "be first") and to do things well, because that is the most important thing to me. Be first? Nice, but definitely NOT important.

All of these words just to say that this is not my modus operandi.

Now, without regard to FusionCache and without intent to offend anyone.

Don't worry, no offense taken, at least on my side 🙂

Lessons learned for building DLLs:

  • never rely on things still not in production OR support them in a separate package (thank you for giving the idea!)

Totally agree! Also, if the new things some lib author is doing are still "experimental", there's an attribute for that (btw originally proposed by yours truly 😬).

Just want to point out that in this specific case I did not "rely on things still not in production", as explained above.

Jody, I reassure you that no matter what your decision will be, our support and gratitude for your efforts will not become less.

Thank you very much for FusionCache!

Thanks: I know that is a delicate topic with a lot of ramifications, so I really really appreciate it.

@rcollina
Copy link

rcollina commented Feb 9, 2025

First and foremost, thank you @jodydonetti for your outstanding work. I’ve been following this closely as I’m dealing with the same conundrum as a framework author.

Just my two cents on the matter - unrelated to FusionCache.

Our in-house framework has several dependencies on libraries like MySql.Data, MySqlConnector, MongoDb, SQL Server Client, npgsql, Kafka, OpenTelemetry and of course Microsoft’s Platform Extensions. I’m sure I missed some. No, I definitely missed some - the expanded dependency tree is bound to surprise me if I were to examine each package recursively.

It’s becoming borderline impossible to match the TFM’s major version with an arbitrarily complex dependency tree.

I’ve been trying to abide by the “major versions must match” rule but I’m seeing a general ecosystem shift towards whatever new version is out for updates.

Unsurprisingly, this implies a non-zero probability of introducing breaking changes that creep up transitively; for example, some Microsoft Platform Extension (can’t remember which off the top of my head) had a different nullability annotation that caused a compilation error. It’s nothing to write home about, I can fix that for my own codebase - but I can’t do the same for consumers of my framework, which will get a breaking change for free, as it were. Long story short, this has a real implication on how we approach versioning as library and framework authors.

In all honesty, I wish Microsoft kept the Platform Extension updates in line with the TFM, so that the whole ecosystem would follow suit.

However, I’m afraid that particular ship has sailed, and whatever the decision, some are bound to be disappointed. I for one will attempt to introduce dependencies with versions >= TFM since I believe I’ll be forced to in the short term.

Again, just my 2c - thank you all for the great comments so far.

@jodydonetti
Copy link
Collaborator

Thanks @rcollina for sharing you experience here, it adds to the mix of knowledge to try and get to some overall guidance, all things considered.

And yes: when implemented, the compatibility matrix with multiple TFMs can become quite crazy, quite fast 🥲

Again, really appreciate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants