using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
///
/// Implements a on top of
///
///
/// The HttpContext is not thread safe and no part of it is which means we need to include our own thread
/// safety mechanisms. This relies on notifications: and
/// in order to facilitate the correct locking and releasing allocations.
///
///
public class HttpContextRequestAppCache : FastDictionaryAppCacheBase, IRequestCache, IDisposable
{
//private static readonly string s_contextItemsLockKey = $"{typeof(HttpContextRequestAppCache).FullName}::LockEntered";
private readonly IHttpContextAccessor _httpContextAccessor;
///
/// Initializes a new instance of the class with a context, for unit tests!
///
public HttpContextRequestAppCache(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
public bool IsAvailable => TryGetContextItems(out _);
private bool TryGetContextItems(out IDictionary items)
{
items = _httpContextAccessor.HttpContext?.Items;
return items != null;
}
///
public override object Get(string key, Func factory)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items))
{
return factory();
}
key = GetCacheKey(key);
Lazy result;
try
{
EnterWriteLock();
result = items[key] as Lazy; // null if key not found
// cannot create value within the lock, so if result.IsValueCreated is false, just
// do nothing here - means that if creation throws, a race condition could cause
// more than one thread to reach the return statement below and throw - accepted.
if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null
{
result = SafeLazy.GetSafeLazy(factory);
items[key] = result;
}
}
finally
{
ExitWriteLock();
}
// using GetSafeLazy and GetSafeLazyValue ensures that we don't cache
// exceptions (but try again and again) and silently eat them - however at
// some point we have to report them - so need to re-throw here
// this does not throw anymore
//return result.Value;
var value = result.Value; // will not throw (safe lazy)
if (value is SafeLazy.ExceptionHolder eh)
eh.Exception.Throw(); // throw once!
return value;
}
public bool Set(string key, object value)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items))
return false;
key = GetCacheKey(key);
try
{
EnterWriteLock();
items[key] = SafeLazy.GetSafeLazy(() => value);
}
finally
{
ExitWriteLock();
}
return true;
}
public bool Remove(string key)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items))
return false;
key = GetCacheKey(key);
try
{
EnterWriteLock();
items.Remove(key);
}
finally
{
ExitWriteLock();
}
return true;
}
#region Entries
protected override IEnumerable> GetDictionaryEntries()
{
const string prefix = CacheItemPrefix + "-";
if (!TryGetContextItems(out var items))
return Enumerable.Empty>();
return items.Cast>()
.Where(x => x.Key is string s && s.StartsWith(prefix));
}
protected override void RemoveEntry(string key)
{
if (!TryGetContextItems(out var items))
return;
items.Remove(key);
}
protected override object GetEntry(string key)
{
return !TryGetContextItems(out var items) ? null : items[key];
}
#endregion
#region Lock
protected override void EnterReadLock()
{
object locker = GetLock();
if (locker == null)
{
return;
}
Monitor.Enter(locker);
}
protected override void EnterWriteLock()
{
object locker = GetLock();
if (locker == null)
{
return;
}
Monitor.Enter(locker);
}
protected override void ExitReadLock()
{
object locker = GetLock();
if (locker == null)
{
return;
}
if (Monitor.IsEntered(locker))
{
Monitor.Exit(locker);
}
}
protected override void ExitWriteLock()
{
object locker = GetLock();
if (locker == null)
{
return;
}
if (Monitor.IsEntered(locker))
{
Monitor.Exit(locker);
}
}
#endregion
public IEnumerator> GetEnumerator()
{
if (!TryGetContextItems(out IDictionary items))
{
yield break;
}
foreach (KeyValuePair item in items)
{
yield return new KeyValuePair(item.Key.ToString(), item.Value);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
/// Ensures and returns the current lock
///
///
private object GetLock()
{
HttpContext httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null)
{
return null;
}
RequestLock requestLock = httpContext.Features.Get();
if (requestLock != null)
{
return requestLock.SyncRoot;
}
IFeatureCollection features = httpContext.Features;
HttpResponse response = httpContext.Response;
lock (httpContext)
{
requestLock = new RequestLock();
features.Set(requestLock);
return requestLock.SyncRoot;
}
}
// This is not a typical dispose pattern since this can be called multiple times to dispose
// whatever might be in the current context.
public void Dispose()
{
// need to resolve from request services since IRequestCache is a non DI service and we don't have a logger when created
ILogger logger = _httpContextAccessor.HttpContext?.RequestServices?.GetRequiredService>();
foreach (KeyValuePair i in this)
{
// NOTE: All of these will be Lazy since that is how this cache works,
// but we'll include the 2nd check too
if (i.Value is Lazy lazy && lazy.IsValueCreated && lazy.Value is IDisposeOnRequestEnd doer1)
{
try
{
doer1.Dispose();
}
catch (Exception ex)
{
logger.LogError("Could not dispose item with key " + i.Key, ex);
}
}
else if (i.Value is IDisposeOnRequestEnd doer2)
{
try
{
doer2.Dispose();
}
catch (Exception ex)
{
logger.LogError("Could not dispose item with key " + i.Key, ex);
}
}
}
}
///
/// Used as Scoped instance to allow locking within a request
///
private class RequestLock
{
public object SyncRoot { get; } = new object();
}
}
}