Skip to content

[BUG] ShouldEagerlyRefresh: System.InvalidOperationException: Nullable object must have a value #426

@jodydonetti

Description

@jodydonetti

Discussed in #417

Originally posted by HannaHromenko March 5, 2025

FusionCache: EagerExpirationTimestamp Becomes Null After Being Checked

Issue Description

We have been using FusionCache for a while, and from time to time, we encounter the following exception:

System.InvalidOperationException: Nullable object must have a value.  
   at ZiggyCreatures.Caching.Fusion.Internals.FusionCacheInternalUtils.ShouldEagerlyRefresh(IFusionCacheEntry entry)  
   in /_/src/ZiggyCreatures.FusionCache/Internals/FusionCacheInternalUtils.cs:line 124  

From the error message, it is clear that the issue is related to EagerRefresh. Specifically, EagerExpirationTimestamp appears to be null at the time of access, even though there is a check beforehand to verify if it has a value.

Code Analysis

Here’s the FusionCache method where the exception is thrown:

public static bool ShouldEagerlyRefresh(this IFusionCacheEntry? entry)
	{
		if (entry?.Metadata is null)
			return false;

		if (entry.Metadata.EagerExpirationTimestamp.HasValue == false)
			return false;

		if (entry.Metadata.EagerExpirationTimestamp.Value >= DateTimeOffset.UtcNow.UtcTicks) // According to the error log it is thrown here because the value is null even thought we checked it for nullability before
			return false;

		if (entry.IsLogicallyExpired())
			return false;

		return true;
	}

According to the error log, EagerExpirationTimestamp.HasValue initially returns true, but by the time the next line accesses entry.Metadata.EagerExpirationTimestamp.Value, it becomes null, causing the exception.

Observations

We use EagerRefresh in two different places. Both trigger the error:

1. First place where cache is used

private readonly FusionCacheEntryOptions _slidingEntryOptions = new FusionCacheEntryOptions()
    .SetDuration(TimeSpan.FromSeconds(60))
    .SetFailSafe(true)
    .SetFactoryTimeouts(TimeSpan.FromMilliseconds(50))
    .SetEagerRefresh(0.8f);

public async Task<Dictionary<string, Result>> GetCached(string cacheKey, Func<CancellationToken, Task<IEnumerable<Result>>> factory, CancellationToken ct)
{
    return await cache.GetOrSetAsync<Dictionary<string, Result>>(cacheKey,
        async (context, token) =>
        {
            return await factory(ct);
        }, _slidingEntryOptions, token: ct);
}

2. Second place where code is used

private readonly FusionCacheEntryOptions _slidingEntryOptions = new FusionCacheEntryOptions()
    .SetDuration(TimeSpan.FromMinutes(5))
    .SetFailSafe(true)
    .SetFactoryTimeouts(TimeSpan.FromMilliseconds(100))
    .SetEagerRefresh(0.8f);

public async Task<Result?> GetCached(VersionRulesCacheKey cacheKey, Func<string, CancellationToken, Task<Result?>> factory, CancellationToken ct)
{
    return await cache.GetOrSetAsync<VersionRules?>(cacheKey, async (cancellationToken) =>
    {
        return await factory(cacheKey.VersionId, cancellationToken);
    }, _slidingEntryOptions, token: ct);
}

Questions

  1. What could cause EagerExpirationTimestamp to become null between the HasValue check and its access?

Any insights would be greatly appreciated! 🚀

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions