// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache { /// /// Represents the default cache policy. /// /// The type of the entity. /// The type of the identifier. /// /// The default cache policy caches entities with a 5 minutes sliding expiration. /// Each entity is cached individually. /// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. /// If options.GetAllCacheValidateCount then we check against the db when getting many entities. /// public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IEntity { private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) : base(cache, scopeAccessor) { _options = options ?? throw new ArgumentNullException(nameof(options)); } protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; protected string GetEntityCacheKey(TId id) { if (EqualityComparer.Default.Equals(id, default)) { return string.Empty; } if (typeof(TId).IsValueType) { return EntityTypeCacheKey + id; } else { return EntityTypeCacheKey + id.ToString().ToUpperInvariant(); } } protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; protected virtual void InsertEntity(string cacheKey, TEntity entity) => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); protected virtual void InsertEntities(TId[] ids, TEntity[] entities) { if (ids.Length == 0 && entities.Length == 0 && _options.GetAllCacheAllowZeroCount) { // getting all of them, and finding nothing. // if we can cache a zero count, cache an empty array, // for as long as the cache is not cleared (no expiration) Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); } else { // individually cache each item foreach (var entity in entities) { var capture = entity; Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); } } } /// public override void Create(TEntity entity, Action persistNew) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { persistNew(entity); // just to be safe, we cannot cache an item without an identity if (entity.HasIdentity) { Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared Cache.Clear(EntityTypeCacheKey); } catch { // if an exception is thrown we need to remove the entry from cache, // this is ONLY a work around because of the way // that we cache entities: http://issues.umbraco.org/issue/U4-4259 Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared Cache.Clear(EntityTypeCacheKey); throw; } } /// public override void Update(TEntity entity, Action persistUpdated) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { persistUpdated(entity); // just to be safe, we cannot cache an item without an identity if (entity.HasIdentity) { Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared Cache.Clear(EntityTypeCacheKey); } catch { // if an exception is thrown we need to remove the entry from cache, // this is ONLY a work around because of the way // that we cache entities: http://issues.umbraco.org/issue/U4-4259 Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared Cache.Clear(EntityTypeCacheKey); throw; } } /// public override void Delete(TEntity entity, Action persistDeleted) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { persistDeleted(entity); } finally { // whatever happens, clear the cache var cacheKey = GetEntityCacheKey(entity.Id); Cache.Clear(cacheKey); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared Cache.Clear(EntityTypeCacheKey); } } /// public override TEntity Get(TId id, Func performGet, Func> performGetAll) { var cacheKey = GetEntityCacheKey(id); var fromCache = Cache.GetCacheItem(cacheKey); // if found in cache then return else fetch and cache if (fromCache != null) { return fromCache; } var entity = performGet(id); if (entity != null && entity.HasIdentity) { InsertEntity(cacheKey, entity); } return entity; } /// public override TEntity GetCached(TId id) { var cacheKey = GetEntityCacheKey(id); return Cache.GetCacheItem(cacheKey); } /// public override bool Exists(TId id, Func performExists, Func> performGetAll) { // if found in cache the return else check var cacheKey = GetEntityCacheKey(id); var fromCache = Cache.GetCacheItem(cacheKey); return fromCache != null || performExists(id); } /// public override TEntity[] GetAll(TId[] ids, Func> performGetAll) { if (ids.Length > 0) { // try to get each entity from the cache // if we can find all of them, return var entities = ids.Select(GetCached).WhereNotNull().ToArray(); if (ids.Length.Equals(entities.Length)) return entities; // no need for null checks, we are not caching nulls } else { // get everything we have var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey) .ToArray(); // no need for null checks, we are not caching nulls if (entities.Length > 0) { // if some of them were in the cache... if (_options.GetAllCacheValidateCount) { // need to validate the count, get the actual count and return if ok var totalCount = _options.PerformCount(); if (entities.Length == totalCount) return entities; } else { // no need to validate, just return what we have and assume it's all there is return entities; } } else if (_options.GetAllCacheAllowZeroCount) { // if none of them were in the cache // and we allow zero count - check for the special (empty) entry var empty = Cache.GetCacheItem(EntityTypeCacheKey); if (empty != null) return empty; } } // cache failed, get from repo and cache var repoEntities = performGetAll(ids) .WhereNotNull() // exclude nulls! .Where(x => x.HasIdentity) // be safe, though would be weird... .ToArray(); // note: if empty & allow zero count, will cache a special (empty) entry InsertEntities(ids, repoEntities); return repoEntities; } /// public override void ClearAll() { Cache.ClearByKey(EntityTypeCacheKey); } } }