-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Should CacheExtensions.GetOrCreate()
/GetOrCreateAsync()
return TItem
instead of TItem?
?
#77266
Comments
Tagging subscribers to this area: @dotnet/area-extensions-caching Issue DetailsTake this snippet: using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddMemoryCache();
var provider = services.BuildServiceProvider();
var cache = provider.GetRequiredService<IMemoryCache>();
var instance = cache.GetOrCreate("key", entry =>
{
// ...
return new
{
Message = "Hello World"
};
});
Console.WriteLine(instance.Message); On .NET 6.0, you don't get any error or warning as the caching stack was not decorated with nullable annotations. I guess the warning is technically correct because - in theory - nothing prevents me from inserting a To make them easier to use, should
|
@eerhardt what you think about this one? |
This looks like the opposite of the request in #77264. In #77264, it is showing that you can cache If someone cached So I believe the existing annotation is correct. |
No, the two are actually unrelated (being able to cache
I mentioned that in my OP but my point is that cache.Set<object?>("key", null);
var instance = cache.GetOrCreate("key", entry =>
{
// ...
return new
{
Message = "Hello World"
};
}); While technically correct, it semantically doesn't make much sense that the nullability of the returned type is not respected when it's used alone: var instance = cache.GetOrCreate("key", entry =>
{
// ...
return new
{
Message = "Hello World"
};
});
// You don't expect a null value here as the delegate doesn't return null. |
And BTW, using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddMemoryCache();
var provider = services.BuildServiceProvider();
var cache = provider.GetRequiredService<IMemoryCache>();
cache.Set<object?>("key", null);
// The returned type is inferred as "int" based on the delegate signature.
var instance = cache.GetOrCreate("key", entry =>
{
// ...
return 10;
});
Console.WriteLine(instance); If you run this snippet, all you'll get is a null-reference exception when calling |
The point of nullable annotations is to be able to answer questions like:
From the method signature. And to statically warn you at compile time when it is possible for you to get NullReferenceExceptions at runtime. If this API was annotated the way you are suggesting, it would be possible for your code to get NREs at runtime, with no warnings at compile time. This goes against the spirit of the annotations. So while it is not likely, or not often the case, it is still possible. This is why it is annotated this way. If |
This is another disadvantage of MemoryCache not being generic. You are trying to use strong-typing, but everything is weakly typed underneath. So there are no guarantees your strong typing will work. |
This issue has been marked |
Per @eerhardt replies, it looks preferable to not do the suggested change. @kevinchalet I am closing the issue but feel free to reply if you still have any questions. |
I maintain a large project that was among the very first third-party projects to adopt nullable references, so believe me, I have a bit of experience regarding how it works 😄 I don't think things are always black or white. There are tons of examples in .NET or ASP.NET Core where properties were deliberately marked non-nullable based on the context even though they can technically be set to Basically most of the "options" classes work this way. For instance, https://github.com/dotnet/aspnetcore/blob/9a765c84d38d3dcc6beab7233e7908b163aa9517/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs#L246 is not nullable and is later initialized by an It's also interesting to note that reducing noisy warning also led to adopting oblivious return types. For instance, Anyway, it's not a big deal. But I wouldn't be surprised if you had similar remarks in the future. |
One of our overarching principles for nullable annotations in the core libraries it that they should never lie and say null isn't a possible result when in fact it is. This does lead to some cases where a method is annotated as returning a |
In many cases, I don't doubt it's true, but I'm still convinced there are cases where you can't guarantee that without considering the context around the API you're calling. Take using System.Collections.Concurrent;
var dictionary = new ConcurrentDictionary<string, Record>();
dictionary.TryAdd("key", null!);
var result = dictionary.GetOrAdd("key", key =>
{
// ...
return new Record("Hello World");
});
Console.WriteLine(result.Message);
public record Record(string Message); Since Following your logic,
I indeed mentioned one of them ( |
But that's because the user of the dictionary lied: dictionary.TryAdd("key", null!); by using null suppression. Once you do that, all bets are off, and no nullable annotation can be correct. |
Sure, but it can be more subtle than that, for instance when the dictionary is passed to code that isn't nullable-aware ; e.g a third-party helper that injects var dictionary = new ConcurrentDictionary<string, Record>();
ThirdPartyAssembly.Helpers.PopulateDictionary(dictionary); In this case, neither the author of the third-party library nor the author of this snippet is suppressing any nullable-related warning so there's no way for any of them to know that (I guess we just disagree on how strong nullability guarantees are: to me, the context matters a lot) Anyway, I appreciate your insights 😃 |
Such code is still violating the nullability contract the developer of
👍 |
I agree, but the "nullability contract" you're referring to here is not something "concrete" that can be verified by the compiler and that materializes to the user as nullability warnings, so it's actually more like a "functional-mental" contract where you basically pray for everything in the chain to do the right thing. And once you say that, it's not very far from the example in my OP, where you also expect nothing/nobody will inject a I get the distinction between these cases is quite subtle (and that's why I personally think we should never consider nullable annotations as super-strong guarantees). We obviously have different perspectives but it's a very interesting conversion so thanks for that. |
Take this snippet:
On .NET 6.0, you don't get any error or warning as the caching stack was not decorated with nullable annotations.
On .NET 7.0, you get a CS8602 warning on the last line, as
GetOrCreate()
returnsTItem?
even if the delegate itself will never return null.I guess the warning is technically correct because - in theory - nothing prevents me from inserting a
null
value in the cache before the delegate has a chance to be called, but I'm not sure it's a frequent case (in this case, it would be more logical that the delegate signature useTItem?
instead ofTItem
to account for potential null values).To make them easier to use, should
CacheExtensions.GetOrCreate()
/GetOrCreateAsync()
returnTItem
instead ofTItem?
?The text was updated successfully, but these errors were encountered: