Files
Umbraco-CMS/src/Umbraco.Core/Cache/HttpRequestAppCache.cs

194 lines
6.3 KiB
C#
Raw Normal View History

2018-06-29 19:52:40 +02:00
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
2019-02-14 08:46:53 +01:00
using System.Threading;
2018-06-29 19:52:40 +02:00
namespace Umbraco.Core.Cache
{
/// <summary>
2019-01-17 11:01:23 +01:00
/// Implements a fast <see cref="IAppCache"/> on top of HttpContext.Items.
2018-06-29 19:52:40 +02:00
/// </summary>
/// <remarks>
2019-01-17 11:01:23 +01:00
/// <para>If no current HttpContext items can be found (no current HttpContext,
/// or no Items...) then this cache acts as a pass-through and does not cache
/// anything.</para>
2018-06-29 19:52:40 +02:00
/// </remarks>
public class HttpRequestAppCache : FastDictionaryAppCacheBase, IRequestCache
2018-06-29 19:52:40 +02:00
{
private static object _syncRoot = new object(); // Using this for locking as the SyncRoot property is not available to us
// on the provided collection provided from .NET Core's HttpContext.Items dictionary,
// as it doesn't implement ICollection where SyncRoot is defined.
2019-01-17 11:01:23 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="HttpRequestAppCache"/> class with a context, for unit tests!
/// </summary>
public HttpRequestAppCache(Func<IDictionary<object, object>> requestItems) : base()
2018-06-29 19:52:40 +02:00
{
ContextItems = requestItems;
2018-06-29 19:52:40 +02:00
}
private Func<IDictionary<object, object>> ContextItems { get; }
2018-06-29 19:52:40 +02:00
public bool IsAvailable => TryGetContextItems(out _);
private bool TryGetContextItems(out IDictionary<object, object> items)
{
items = ContextItems?.Invoke();
return items != null;
}
2019-01-17 11:01:23 +01:00
/// <inheritdoc />
public override object Get(string key, Func<object> factory)
2018-06-29 19:52:40 +02:00
{
2019-01-17 11:01:23 +01:00
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return factory();
2019-01-17 11:01:23 +01:00
key = GetCacheKey(key);
Lazy<object> result;
try
{
EnterWriteLock();
result = items[key] as Lazy<object>; // null if key not found
2019-01-17 11:01:23 +01:00
// 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
2019-01-17 11:01:23 +01:00
{
result = SafeLazy.GetSafeLazy(factory);
items[key] = result;
2019-01-17 11:01:23 +01:00
}
}
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!
2019-01-17 11:01:23 +01:00
return value;
2018-06-29 19:52:40 +02:00
}
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;
}
2019-01-17 11:01:23 +01:00
#region Entries
protected override IEnumerable<KeyValuePair<object, object>> GetDictionaryEntries()
2018-06-29 19:52:40 +02:00
{
const string prefix = CacheItemPrefix + "-";
if (!TryGetContextItems(out var items)) return Enumerable.Empty<KeyValuePair<object, object>>();
2018-06-29 19:52:40 +02:00
return items.Cast<KeyValuePair<object, object>>()
2019-01-17 11:01:23 +01:00
.Where(x => x.Key is string s && s.StartsWith(prefix));
2018-06-29 19:52:40 +02:00
}
protected override void RemoveEntry(string key)
{
if (!TryGetContextItems(out var items)) return;
2018-06-29 19:52:40 +02:00
items.Remove(key);
2018-06-29 19:52:40 +02:00
}
protected override object GetEntry(string key)
{
return !TryGetContextItems(out var items) ? null : items[key];
2018-06-29 19:52:40 +02:00
}
2019-01-17 11:01:23 +01:00
#endregion
2018-06-29 19:52:40 +02:00
#region Lock
2019-02-14 08:46:53 +01:00
private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered";
2018-06-29 19:52:40 +02:00
protected override void EnterReadLock() => EnterWriteLock();
protected override void EnterWriteLock()
{
if (!TryGetContextItems(out var items)) return;
2019-02-14 08:46:53 +01:00
// note: cannot keep 'entered' as a class variable here,
// since there is one per request - so storing it within
// ContextItems - which is locked, so this should be safe
var entered = false;
Monitor.Enter(_syncRoot, ref entered);
items[ContextItemsLockKey] = entered;
2018-06-29 19:52:40 +02:00
}
protected override void ExitReadLock() => ExitWriteLock();
protected override void ExitWriteLock()
{
if (!TryGetContextItems(out var items)) return;
var entered = (bool?)items[ContextItemsLockKey] ?? false;
2019-02-14 08:46:53 +01:00
if (entered)
Monitor.Exit(_syncRoot);
items.Remove(ContextItemsLockKey);
2018-06-29 19:52:40 +02:00
}
#endregion
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
if (!TryGetContextItems(out var items))
{
yield break;
}
foreach (var item in items)
{
yield return new KeyValuePair<string, object>(item.Key.ToString(), item.Value);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
2018-06-29 19:52:40 +02:00
}
}