2021-03-03 10:40:16 +11:00
|
|
|
using System;
|
2020-04-20 06:19:59 +02:00
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
2021-03-03 10:40:16 +11:00
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2021-03-05 15:27:45 +11:00
|
|
|
using Microsoft.Extensions.Logging;
|
2021-03-03 10:40:16 +11:00
|
|
|
using Umbraco.Cms.Core.Events;
|
|
|
|
|
using Umbraco.Extensions;
|
2020-04-20 06:19:59 +02:00
|
|
|
|
2021-02-18 11:06:02 +01:00
|
|
|
namespace Umbraco.Cms.Core.Cache
|
2020-04-20 06:19:59 +02:00
|
|
|
{
|
|
|
|
|
/// <summary>
|
2021-03-03 10:40:16 +11:00
|
|
|
/// Implements a <see cref="IAppCache"/> on top of <see cref="IHttpContextAccessor"/>
|
2020-04-20 06:19:59 +02:00
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
2021-03-03 10:40:16 +11:00
|
|
|
/// <para>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: <see cref="UmbracoRequestBegin"/> and <see cref="UmbracoRequestEnd"/>
|
|
|
|
|
/// in order to facilitate the correct locking and releasing allocations.
|
|
|
|
|
/// </para>
|
2020-04-20 06:19:59 +02:00
|
|
|
/// </remarks>
|
2021-03-05 15:27:45 +11:00
|
|
|
public class HttpContextRequestAppCache : FastDictionaryAppCacheBase, IRequestCache, IDisposable
|
2020-04-20 06:19:59 +02:00
|
|
|
{
|
2021-03-03 10:40:16 +11:00
|
|
|
//private static readonly string s_contextItemsLockKey = $"{typeof(HttpContextRequestAppCache).FullName}::LockEntered";
|
|
|
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
|
|
|
|
2020-04-20 06:19:59 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes a new instance of the <see cref="HttpRequestAppCache"/> class with a context, for unit tests!
|
|
|
|
|
/// </summary>
|
2021-03-05 15:27:45 +11:00
|
|
|
public HttpContextRequestAppCache(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
|
2020-04-20 06:19:59 +02:00
|
|
|
|
|
|
|
|
public bool IsAvailable => TryGetContextItems(out _);
|
|
|
|
|
|
|
|
|
|
private bool TryGetContextItems(out IDictionary<object, object> items)
|
|
|
|
|
{
|
2021-03-03 10:40:16 +11:00
|
|
|
items = _httpContextAccessor.HttpContext?.Items;
|
2020-04-20 06:19:59 +02:00
|
|
|
return items != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public override object Get(string key, Func<object> factory)
|
|
|
|
|
{
|
|
|
|
|
//no place to cache so just return the callback result
|
2021-03-03 10:40:16 +11:00
|
|
|
if (!TryGetContextItems(out var items))
|
|
|
|
|
{
|
|
|
|
|
return factory();
|
|
|
|
|
}
|
2020-04-20 06:19:59 +02:00
|
|
|
|
|
|
|
|
key = GetCacheKey(key);
|
|
|
|
|
|
|
|
|
|
Lazy<object> result;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
EnterWriteLock();
|
|
|
|
|
result = items[key] as Lazy<object>; // 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)
|
2021-03-05 15:27:45 +11:00
|
|
|
if (value is SafeLazy.ExceptionHolder eh)
|
|
|
|
|
eh.Exception.Throw(); // throw once!
|
2020-04-20 06:19:59 +02:00
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool Set(string key, object value)
|
|
|
|
|
{
|
|
|
|
|
//no place to cache so just return the callback result
|
2021-03-05 15:27:45 +11:00
|
|
|
if (!TryGetContextItems(out var items))
|
|
|
|
|
return false;
|
2020-04-20 06:19:59 +02:00
|
|
|
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
|
2021-03-05 15:27:45 +11:00
|
|
|
if (!TryGetContextItems(out var items))
|
|
|
|
|
return false;
|
2020-04-20 06:19:59 +02:00
|
|
|
key = GetCacheKey(key);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
EnterWriteLock();
|
|
|
|
|
items.Remove(key);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
ExitWriteLock();
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Entries
|
|
|
|
|
|
2020-10-26 14:26:49 +01:00
|
|
|
protected override IEnumerable<KeyValuePair<object, object>> GetDictionaryEntries()
|
2020-04-20 06:19:59 +02:00
|
|
|
{
|
|
|
|
|
const string prefix = CacheItemPrefix + "-";
|
|
|
|
|
|
2021-03-05 15:27:45 +11:00
|
|
|
if (!TryGetContextItems(out var items))
|
|
|
|
|
return Enumerable.Empty<KeyValuePair<object, object>>();
|
2020-04-20 06:19:59 +02:00
|
|
|
|
2020-10-26 14:26:49 +01:00
|
|
|
return items.Cast<KeyValuePair<object, object>>()
|
2020-04-20 06:19:59 +02:00
|
|
|
.Where(x => x.Key is string s && s.StartsWith(prefix));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void RemoveEntry(string key)
|
|
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
if (!TryGetContextItems(out var items))
|
|
|
|
|
return;
|
2020-04-20 06:19:59 +02:00
|
|
|
|
|
|
|
|
items.Remove(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override object GetEntry(string key)
|
|
|
|
|
{
|
|
|
|
|
return !TryGetContextItems(out var items) ? null : items[key];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Lock
|
|
|
|
|
|
2021-03-03 10:40:16 +11:00
|
|
|
protected override void EnterReadLock()
|
|
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
object locker = GetLock();
|
|
|
|
|
if (locker == null)
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-05 15:27:45 +11:00
|
|
|
Monitor.Enter(locker);
|
2021-03-03 10:40:16 +11:00
|
|
|
}
|
2020-04-20 06:19:59 +02:00
|
|
|
|
|
|
|
|
protected override void EnterWriteLock()
|
|
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
object locker = GetLock();
|
|
|
|
|
if (locker == null)
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-05 15:27:45 +11:00
|
|
|
Monitor.Enter(locker);
|
2020-04-20 06:19:59 +02:00
|
|
|
}
|
|
|
|
|
|
2021-03-03 10:40:16 +11:00
|
|
|
protected override void ExitReadLock()
|
|
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
object locker = GetLock();
|
|
|
|
|
if (locker == null)
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-05 15:27:45 +11:00
|
|
|
if (Monitor.IsEntered(locker))
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
Monitor.Exit(locker);
|
2021-03-03 10:40:16 +11:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-20 06:19:59 +02:00
|
|
|
|
|
|
|
|
protected override void ExitWriteLock()
|
|
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
object locker = GetLock();
|
|
|
|
|
if (locker == null)
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-05 15:27:45 +11:00
|
|
|
if (Monitor.IsEntered(locker))
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
Monitor.Exit(locker);
|
2021-03-03 10:40:16 +11:00
|
|
|
}
|
2020-04-20 06:19:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
|
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
if (!TryGetContextItems(out IDictionary<object, object> items))
|
2020-04-20 06:19:59 +02:00
|
|
|
{
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-05 15:27:45 +11:00
|
|
|
foreach (KeyValuePair<object, object> item in items)
|
2020-04-20 06:19:59 +02:00
|
|
|
{
|
|
|
|
|
yield return new KeyValuePair<string, object>(item.Key.ToString(), item.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
2021-03-03 10:40:16 +11:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Ensures and returns the current lock
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
2021-03-05 15:27:45 +11:00
|
|
|
private object GetLock()
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
HttpContext httpContext = _httpContextAccessor.HttpContext;
|
|
|
|
|
if (httpContext == null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RequestLock requestLock = httpContext.Features.Get<RequestLock>();
|
|
|
|
|
if (requestLock != null)
|
|
|
|
|
{
|
|
|
|
|
return requestLock.SyncRoot;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IFeatureCollection features = httpContext.Features;
|
|
|
|
|
HttpResponse response = httpContext.Response;
|
2021-03-03 10:40:16 +11:00
|
|
|
|
2021-03-05 15:27:45 +11:00
|
|
|
lock (httpContext)
|
|
|
|
|
{
|
|
|
|
|
requestLock = new RequestLock();
|
|
|
|
|
features.Set(requestLock);
|
|
|
|
|
return requestLock.SyncRoot;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-03 10:40:16 +11:00
|
|
|
|
2021-03-05 15:27:45 +11:00
|
|
|
// 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<HttpContextRequestAppCache> logger = _httpContextAccessor.HttpContext?.RequestServices?.GetRequiredService<ILogger<HttpContextRequestAppCache>>();
|
|
|
|
|
foreach (KeyValuePair<string, object> i in this)
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
// NOTE: All of these will be Lazy<object> since that is how this cache works,
|
|
|
|
|
// but we'll include the 2nd check too
|
|
|
|
|
if (i.Value is Lazy<object> lazy && lazy.IsValueCreated && lazy.Value is IDisposeOnRequestEnd doer1)
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
try
|
2021-03-03 10:40:16 +11:00
|
|
|
{
|
2021-03-05 15:27:45 +11:00
|
|
|
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);
|
2021-03-03 10:40:16 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-05 15:27:45 +11:00
|
|
|
}
|
2021-03-03 10:40:16 +11:00
|
|
|
|
2021-03-05 15:27:45 +11:00
|
|
|
/// <summary>
|
|
|
|
|
/// Used as Scoped instance to allow locking within a request
|
|
|
|
|
/// </summary>
|
|
|
|
|
private class RequestLock
|
|
|
|
|
{
|
|
|
|
|
public object SyncRoot { get; } = new object();
|
2021-03-03 10:40:16 +11:00
|
|
|
}
|
2020-04-20 06:19:59 +02:00
|
|
|
}
|
|
|
|
|
}
|