using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Scoping; namespace Umbraco.Core.Cache { /// /// Represents a caching policy that caches the entire entities set as a single collection. /// /// The type of the entity. /// The type of the identifier. /// /// Caches the entire set of entities as a single collection. /// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, /// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to /// keep as a whole in memory. /// internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IEntity { private readonly Func _entityGetId; private readonly bool _expires; public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires) : base(cache, scopeAccessor) { _entityGetId = entityGetId; _expires = expires; } protected static readonly TId[] EmptyIds = new TId[0]; // const protected string GetEntityTypeCacheKey() { return $"uRepo_{typeof (TEntity).Name}_"; } protected void InsertEntities(TEntity[] entities) { // cache is expected to be a deep-cloning cache ie it deep-clones whatever is // IDeepCloneable when it goes in, and out. it also resets dirty properties, // making sure that no 'dirty' entity is cached. // // this policy is caching the entire list of entities. to ensure that entities // are properly deep-clones when cached, it uses a DeepCloneableList. however, // we don't want to deep-clone *each* entity in the list when fetching it from // cache as that would not be efficient for Get(id). so the DeepCloneableList is // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, // and then will *not* clone when retrieving. var key = GetEntityTypeCacheKey(); if (_expires) { Cache.InsertCacheItem(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); } else { Cache.InsertCacheItem(key, () => new DeepCloneableList(entities)); } } /// public override void Create(TEntity entity, Action persistNew) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { persistNew(entity); } finally { ClearAll(); } } /// public override void Update(TEntity entity, Action persistUpdated) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { persistUpdated(entity); } finally { ClearAll(); } } /// public override void Delete(TEntity entity, Action persistDeleted) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { persistDeleted(entity); } finally { ClearAll(); } } /// public override TEntity Get(TId id, Func performGet, Func> performGetAll) { // get all from the cache, then look for the entity var all = GetAllCached(performGetAll); var entity = all.FirstOrDefault(x => _entityGetId(x).Equals(id)); // see note in InsertEntities - what we get here is the original // cached entity, not a clone, so we need to manually ensure it is deep-cloned. return (TEntity)entity?.DeepClone(); } /// public override TEntity GetCached(TId id) { // get all from the cache -- and only the cache, then look for the entity var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); var entity = all?.FirstOrDefault(x => _entityGetId(x).Equals(id)); // see note in InsertEntities - what we get here is the original // cached entity, not a clone, so we need to manually ensure it is deep-cloned. return (TEntity) entity?.DeepClone(); } /// public override bool Exists(TId id, Func performExits, Func> performGetAll) { // get all as one set, then look for the entity var all = GetAllCached(performGetAll); return all.Any(x => _entityGetId(x).Equals(id)); } /// public override TEntity[] GetAll(TId[] ids, Func> performGetAll) { // get all as one set, from cache if possible, else repo var all = GetAllCached(performGetAll); // if ids have been specified, filter if (ids.Length > 0) all = all.Where(x => ids.Contains(_entityGetId(x))); // and return // see note in SetCacheActionToInsertEntities - what we get here is the original // cached entities, not clones, so we need to manually ensure they are deep-cloned. return all.Select(x => (TEntity) x.DeepClone()).ToArray(); } // does NOT clone anything, so be nice with the returned values private IEnumerable GetAllCached(Func> performGetAll) { // try the cache first var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); if (all != null) return all.ToArray(); // else get from repo and cache var entities = performGetAll(EmptyIds).WhereNotNull().ToArray(); InsertEntities(entities); // may be an empty array... return entities; } /// public override void ClearAll() { Cache.ClearCacheItem(GetEntityTypeCacheKey()); } } }