Here is my C# solution of a temporary storage please check it for any optimization for performance or logic:
Or if there is a built-in type in C# that do the same:

using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;

namespace Test;

public class Cache<TKey, TValue> where TKey : notnull
{
    public TimeSpan DefaultExpireAfter = TimeSpan.FromMinutes(2);
    public int ExpiryCheckIntervalMs = 10_000; //10 Seconds

    private readonly Dictionary<TKey, CacheItem<TValue>> _cache = new();
    private readonly object _lock = new();
    private readonly object _expireLock = new();

    public void Store(TKey key, TValue? value, TimeSpan expiresAfter)
    {
        lock (_lock)
            _cache[key] = new CacheItem<TValue>(value, expiresAfter);

        RemoveExpired();
    }
    public void Store(TKey key, TValue? value)
    {
        lock (_lock)
            _cache[key] = new CacheItem<TValue>(value, DefaultExpireAfter);

        RemoveExpired();
    }

    public TValue? Get(TKey key, bool returnDefaultIfExpired = true)
    {
        lock (_lock)
        {
            if (!_cache.TryGetValue(key, out var item)) return default;

            if (!item.IsExpired)
                return item.Value;

            _cache.Remove(key);
            return returnDefaultIfExpired ? default : item.Value;
        }
    }

    public TValue? Remove(TKey key)
    {
        lock (_lock)
            if (_cache.Remove(key, out var item))
                return item.Value;

        return default;
    }

    public TValue? this[TKey key, bool returnDefaultIfExpired = true]
    {
        get => Get(key, returnDefaultIfExpired);
        set => Store(key, value);
    }
    public TValue? this[TKey key, TimeSpan expiresAfter]
    {
        get => Get(key);
        set => Store(key, value, expiresAfter);
    }

    public void RemoveExpired()
    {
        //Return if already running
        if (!Monitor.TryEnter(_expireLock, 0)) return;
        //_expireLock is taken

        new Thread(RemoveLooper) { IsBackground = true }.Start();
    }

    private void RemoveLooper()
    {
        while (_cache.Count != 0)
        {
            //Get Expired Keys without locking
            var expiredKeys = _cache
                .Where(i => i.Value.IsExpired)
                .Select(i => i.Key)
                .ToArray();

            //Continue if none found
            if (expiredKeys.Length == 0)
            {
                Thread.Sleep(ExpiryCheckIntervalMs); //Sleep
                continue;
            }

            try
            {
                //Lock on _cache's set/get lock
                Monitor.Enter(_lock);

                foreach (var key in expiredKeys)
                {
                    //Check again if item exists (Maybe deleted before locking)
                    if (!_cache.TryGetValue(key, out var item)) continue;

                    //Check if expired again (Maybe updated before locking)
                    if (item.IsExpired)
                        _cache.Remove(key); //Expired -> Remove
                }
            }
            finally
            {
                //Release _lock
                Monitor.Exit(_lock);
            }

            Thread.Sleep(ExpiryCheckIntervalMs); //Sleep
        }

        //Release _expireLock
        Monitor.Exit(_expireLock);
    }

    private class CacheItem<T>
    {
        public T? Value { get; }
        public bool IsExpired => DateTimeOffset.Now - Created >= ExpiresAfter;

        private DateTimeOffset Created { get; }
        private TimeSpan ExpiresAfter { get; }

        public CacheItem(T? value, TimeSpan expiresAfter)
        {
            Value = value;
            Created = DateTimeOffset.Now;
            ExpiresAfter = expiresAfter;
        }
    }
}