/// <summary>
        /// <para>
        /// Gets value corresponding to <paramref name="key"/>.
        /// </para>
        /// <para>
        /// If another initialization function is already running, new initialization function will not be started.
        /// The result will be result of currently running initialization function.
        /// </para>
        /// <para>
        /// If previous initialization function is successfully completed it will return the value. It is possible this
        /// value is stale and will only be updated after the force refresh task is complete.
        /// </para>
        /// <para>
        /// Force refresh is true:
        /// If the key does not exist: It will create and await the new task
        /// If the key exists and the current task is still running: It will return the existing task
        /// If the key exists and the current task is already done: It will start a new task to get the updated values.
        ///     Once the refresh task is complete it will be returned to caller.
        ///     If it is a success the value in the cache will be updated. If the refresh task throws an exception the key will be removed from the cache.
        /// </para>
        /// <para>
        /// If previous initialization function failed - new one will be launched.
        /// </para>
        /// </summary>
        public async Task <TValue> GetAsync(
            TKey key,
            Func <Task <TValue> > singleValueInitFunc,
            bool forceRefresh,
            Action <TValue, TValue> callBackOnForceRefresh)
        {
            if (this.values.TryGetValue(key, out AsyncLazyWithRefreshTask <TValue> initialLazyValue))
            {
                if (!forceRefresh)
                {
                    return(await initialLazyValue.GetValueAsync());
                }

                return(await initialLazyValue.CreateAndWaitForBackgroundRefreshTaskAsync(
                           singleValueInitFunc,
                           () => this.TryRemove(key),
                           callBackOnForceRefresh));
            }

            // The AsyncLazyWithRefreshTask is lazy and won't create the task until GetValue is called.
            // It's possible multiple threads will call the GetOrAdd for the same key. The current asyncLazy may
            // not be used if another thread adds it first.
            AsyncLazyWithRefreshTask <TValue> asyncLazy = new AsyncLazyWithRefreshTask <TValue>(singleValueInitFunc, this.cancellationTokenSource.Token);
            AsyncLazyWithRefreshTask <TValue> result    = this.values.GetOrAdd(
                key,
                asyncLazy);

            // Another thread async lazy was inserted. Just await on the inserted lazy object.
            if (!object.ReferenceEquals(asyncLazy, result))
            {
                return(await result.GetValueAsync());
            }

            // This means the current caller async lazy was inserted into the concurrent dictionary.
            // The task is now awaited on so if an exception occurs it can be removed from
            // the concurrent dictionary.
            try
            {
                return(await result.GetValueAsync());
            }
            catch (Exception e)
            {
                DefaultTrace.TraceError(
                    "AsyncCacheNonBlocking Failed GetAsync with key: {0}, Exception: {1}",
                    key.ToString(),
                    e.ToString());

                // Remove the failed task from the dictionary so future requests can send other calls..
                this.values.TryRemove(key, out _);
                throw;
            }
        }
        /// <summary>
        /// <para>
        /// Gets value corresponding to <paramref name="key"/>.
        /// </para>
        /// <para>
        /// If another initialization function is already running, new initialization function will not be started.
        /// The result will be result of currently running initialization function.
        /// </para>
        /// <para>
        /// If previous initialization function is successfully completed it will return the value. It is possible this
        /// value is stale and will only be updated after the force refresh task is complete.
        /// </para>
        /// <para>
        /// Force refresh is true:
        /// If the key does not exist: It will create and await the new task
        /// If the key exists and the current task is still running: It will return the existing task
        /// If the key exists and the current task is already done: It will start a new task to get the updated values.
        ///     Once the refresh task is complete it will be returned to caller.
        ///     If it is a success the value in the cache will be updated. If the refresh task throws an exception the key will be removed from the cache.
        /// </para>
        /// <para>
        /// If previous initialization function failed - new one will be launched.
        /// </para>
        /// </summary>
        public async Task <TValue> GetAsync(
            TKey key,
            Func <TValue, Task <TValue> > singleValueInitFunc,
            Func <TValue, bool> forceRefresh)
        {
            if (this.values.TryGetValue(key, out AsyncLazyWithRefreshTask <TValue> initialLazyValue))
            {
                try
                {
                    TValue cachedResult = await initialLazyValue.GetValueAsync();

                    if (forceRefresh == null || !forceRefresh(cachedResult))
                    {
                        return(cachedResult);
                    }
                }
                catch (Exception e)
                {
                    // This is needed for scenarios where the initial GetAsync was
                    // called but never awaited.
                    if (initialLazyValue.ShouldRemoveFromCacheThreadSafe())
                    {
                        bool removed = this.TryRemove(key);

                        DefaultTrace.TraceError(
                            "AsyncCacheNonBlocking Failed GetAsync. key: {0}, tryRemoved: {1}, Exception: {2}",
                            key,
                            removed,
                            e);
                    }

                    throw;
                }

                try
                {
                    return(await initialLazyValue.CreateAndWaitForBackgroundRefreshTaskAsync(
                               createRefreshTask : singleValueInitFunc));
                }
                catch (Exception e)
                {
                    if (initialLazyValue.ShouldRemoveFromCacheThreadSafe())
                    {
                        DefaultTrace.TraceError(
                            "AsyncCacheNonBlocking.GetAsync with ForceRefresh Failed. key: {0}, Exception: {1}",
                            key,
                            e);

                        // In some scenarios when a background failure occurs like a 404
                        // the initial cache value should be removed.
                        if (this.removeFromCacheOnBackgroundRefreshException(e))
                        {
                            this.TryRemove(key);
                        }
                    }

                    throw;
                }
            }

            // The AsyncLazyWithRefreshTask is lazy and won't create the task until GetValue is called.
            // It's possible multiple threads will call the GetOrAdd for the same key. The current asyncLazy may
            // not be used if another thread adds it first.
            AsyncLazyWithRefreshTask <TValue> asyncLazy = new AsyncLazyWithRefreshTask <TValue>(singleValueInitFunc, this.cancellationTokenSource.Token);
            AsyncLazyWithRefreshTask <TValue> result    = this.values.GetOrAdd(
                key,
                asyncLazy);

            // Another thread async lazy was inserted. Just await on the inserted lazy object.
            if (!object.ReferenceEquals(asyncLazy, result))
            {
                return(await result.GetValueAsync());
            }

            // This means the current caller async lazy was inserted into the concurrent dictionary.
            // The task is now awaited on so if an exception occurs it can be removed from
            // the concurrent dictionary.
            try
            {
                return(await result.GetValueAsync());
            }
            catch (Exception e)
            {
                DefaultTrace.TraceError(
                    "AsyncCacheNonBlocking Failed GetAsync with key: {0}, Exception: {1}",
                    key.ToString(),
                    e.ToString());

                // Remove the failed task from the dictionary so future requests can send other calls..
                this.values.TryRemove(key, out _);
                throw;
            }
        }