What's wrong
ICacheProvider<TKey,TValue>'s default interface methods Get and GetOrAdd (Essentials/ICacheProvider.cs:52-80) check value is null in addition to the TryGet boolean result:
if (!TryGet(key, out TValue? value) || value is null)
throw new KeyNotFoundException(...);
if (TryGet(key, out TValue? value) && value is not null)
return value;
For a nullable TValue (e.g. string? or a reference type where null is a legitimate cached result), InMemoryCacheProvider<TKey,TValue> and other implementations correctly store and return value = null with TryGet returning true. But the default Get/GetOrAdd (and the async wrappers built on them, GetAsync/GetOrAddAsync) then treat that as "not found": Get throws KeyNotFoundException even though the key was found, and GetOrAdd silently discards the cached null and re-invokes (and re-Sets) the factory on every single call — defeating caching entirely for any factory that can legitimately produce null.
Failure scenario
A consumer caches the (correct) result of a lookup that returns null — e.g. cache.GetOrAdd("missing-user", () => LookupUser(id)) where LookupUser returns User? and legitimately returns null for a nonexistent id. Every call re-invokes LookupUser instead of reusing the cached null, and any caller using Get for that same key gets an unexpected KeyNotFoundException instead of null.
Suggested fix
Use only the bool result from TryGet, not a null check on value:
if (!TryGet(key, out TValue? value))
throw new KeyNotFoundException(...);
...
if (TryGet(key, out TValue? value))
return value!;
Acceptance criteria
A test that seeds a cache entry with a null value via Set(key, null) asserts Get returns null without throwing, and GetOrAdd returns the cached null without invoking the factory again.
What's wrong
ICacheProvider<TKey,TValue>'s default interface methodsGetandGetOrAdd(Essentials/ICacheProvider.cs:52-80) checkvalue is nullin addition to theTryGetboolean result:For a nullable
TValue(e.g.string?or a reference type wherenullis a legitimate cached result),InMemoryCacheProvider<TKey,TValue>and other implementations correctly store and returnvalue = nullwithTryGetreturningtrue. But the defaultGet/GetOrAdd(and the async wrappers built on them,GetAsync/GetOrAddAsync) then treat that as "not found":GetthrowsKeyNotFoundExceptioneven though the key was found, andGetOrAddsilently discards the cached null and re-invokes (and re-Sets) the factory on every single call — defeating caching entirely for any factory that can legitimately produce null.Failure scenario
A consumer caches the (correct) result of a lookup that returns null — e.g.
cache.GetOrAdd("missing-user", () => LookupUser(id))whereLookupUserreturnsUser?and legitimately returns null for a nonexistent id. Every call re-invokesLookupUserinstead of reusing the cached null, and any caller usingGetfor that same key gets an unexpectedKeyNotFoundExceptioninstead of null.Suggested fix
Use only the
boolresult fromTryGet, not a null check onvalue:Acceptance criteria
A test that seeds a cache entry with a null value via
Set(key, null)assertsGetreturns null without throwing, andGetOrAddreturns the cached null without invoking the factory again.