0

I have a website hosted in IIS. I am caching values in a repository to save numerous trips to the database. The cache is instantiated at the top of the repo like so:

protected static List<MaxDeposit> _cached_values = new List<MaxDeposit>();

When a call is made to retrieve data from the db, I'm checking the cache first to see if we already have it:

protected List<MaxDeposit> GetMaxDepositsFromCacheByTier(int tierId)
{
    return _cached_values.Where(x => x.TierId == tierId).ToList();
}

This works as expected and either returns a value or values, or an empty list. If an empty list is returned, the data is retrieved from the db and then added to the cache like so:

protected void AddMaxDepositsToCache(List<MaxDeposit> maxDeposits)
{
     if (maxDeposits == null) return;

     lock (this)
     {
        _cached_values.AddRange(maxDeposits);
     }
 }

This is in production and seems to be working fine for a few days until we start seeing NullReferenceExceptions on the Return statement:

System.NullReferenceException: Object reference not set to an instance of an object.
   at MyProject.Model.Data.Repositories.MaxDepositRepository.<>c__DisplayClass3_0.<GetMaxDepositsFromCacheByTier>b__0(MaxDeposit x) in Data\Repositories\MaxDepositRepository.cs:line 31
   at System.Linq.Enumerable.WhereListIterator<T>.MoveNext()
   at System.Collections.Generic.List<T>..ctor(IEnumerable<T> collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable<T> source)
   at MyProject.Model.Data.Repositories.MaxDepositRepository.GetMaxDepositsFromCacheByTier(Int32 tierId) in Data\Repositories\MaxDepositRepository.cs:line 31
   at MyProject.Model.Data.Repositories.MaxDepositRepository.GetByTier(Int32 tierId) in Data\Repositories\MaxDepositRepository.cs:line 0

Once this happens, every call to this code then throws the same exception. Recycling the application pool resolves the problem and calls are made successfully again.

I assumed that somehow _cached_values ended up becoming NULL (even though at no point in code is it set to NULL), so I modified the code as such:

protected List<MaxDeposit> GetMaxDepositsFromCacheByTier(int tierId)
        {
            if (_cached_values == null)
            {
                _cached_values = new List<MaxDeposit>();

                _logger.LogInformation($"MaxDepositRepository.GetMaxDepositsFromCacheByTier(int tierId): _cached_values field was null. tierId = {tierId}");
            }

            return _cached_values.Where(x => x.TierId == tierId).ToList();
        }

So now I null check _cached_values before working with it, and instantiate a new empty list if it is NULL, and log that we've had to do so. This change is in production, but the same issue happened again yesterday. The added logging was not found, which tells me _cached_values was not NULL at any point.

Is the problem to do with the Linq Where clause? If so, why does it work for a while before breaking?

Thanks in advance if someone is able to explain what's happening here or pinpoint where I'm going wrong!

darrenp1987
  • 1
  • 1
  • 2
  • 1
    What happens if an individual item in `_cached_values` is `null`? You can't get the value of `TierId` if `x` is `null`, right? – ProgrammingLlama Apr 06 '21 at 09:32
  • 1
    Also, you've said that this is a web application. `List` isn't thread safe, so if you're altering it from multiple different threads, weird stuff can happen. [Exanple](https://rextester.com/HXRCB93223). We add items to the list in parallel, ignoring any errors, and despite never adding any `null` objects, we end up with a number of `null` entries. – ProgrammingLlama Apr 06 '21 at 09:33
  • 1
    Since the NRE is mapping inside the `x => x.TierId == tierId` delegate, it's almost certain that `x` there is null. – canton7 Apr 06 '21 at 09:42
  • @Llama Thanks for the example, I think that must be what's going wrong. – darrenp1987 Apr 06 '21 at 09:54
  • @canton7 Yeah thanks, I think you're right. – darrenp1987 Apr 06 '21 at 09:55

0 Answers0