Skip to content

ICacheProvider.Get and GetOrAdd treat a legitimately cached null as a cache miss #30

Description

@matt-edmondson

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions