using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Umbraco.Cms.Core.Notifications; 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 { 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 _); /// public override object? Get(string key, Func factory) { // no place to cache so just return the callback result if (!TryGetContextItems(out IDictionary? 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. // get non-created as NonCreatedValue & exceptions as null if (result == null || SafeLazy.GetSafeLazyValue(result, true) == 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 IDictionary? 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 IDictionary? items)) { return false; } key = GetCacheKey(key); try { EnterWriteLock(); items.Remove(key); } finally { ExitWriteLock(); } return true; } 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(); private bool TryGetContextItems([MaybeNullWhen(false)] out IDictionary items) { items = _httpContextAccessor.HttpContext?.Items; return items != null; } /// /// 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; lock (httpContext) { requestLock = new RequestLock(); features.Set(requestLock); return requestLock.SyncRoot; } } /// /// Used as Scoped instance to allow locking within a request /// private class RequestLock { public object SyncRoot { get; } = new(); } #region Entries protected override IEnumerable> GetDictionaryEntries() { const string prefix = CacheItemPrefix + "-"; if (!TryGetContextItems(out IDictionary? items)) { return Enumerable.Empty>(); } return items .Where(x => x.Value is not null && x.Key is string s && s.StartsWith(prefix))!; } protected override void RemoveEntry(string key) { if (!TryGetContextItems(out IDictionary? items)) { return; } items.Remove(key); } protected override object? GetEntry(string key) => !TryGetContextItems(out IDictionary? items) ? null : items[key]; #endregion #region Lock protected override void EnterReadLock() { var locker = GetLock(); if (locker == null) { return; } Monitor.Enter(locker); } protected override void EnterWriteLock() { var locker = GetLock(); if (locker == null) { return; } Monitor.Enter(locker); } protected override void ExitReadLock() { var locker = GetLock(); if (locker == null) { return; } if (Monitor.IsEntered(locker)) { Monitor.Exit(locker); } } protected override void ExitWriteLock() { var locker = GetLock(); if (locker == null) { return; } if (Monitor.IsEntered(locker)) { Monitor.Exit(locker); } } #endregion }