using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { /// /// A caching policy that caches an entire dataset as a single collection /// /// /// internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { private readonly Func _getEntityId; private readonly Func> _getAllFromRepo; private readonly bool _expires; public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId, Func> getAllFromRepo, bool expires) : base(cache) { _getEntityId = getEntityId; _getAllFromRepo = getAllFromRepo; _expires = expires; } private bool? _hasZeroCountCache; protected string GetCacheTypeKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } public override void CreateOrUpdate(TEntity entity, Action persistMethod) { if (entity == null) throw new ArgumentNullException("entity"); if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { persistMethod(entity); //set the disposal action SetCacheAction(() => { //Clear all Cache.ClearCacheItem(GetCacheTypeKey()); }); } catch { //set the disposal action SetCacheAction(() => { //Clear all Cache.ClearCacheItem(GetCacheTypeKey()); }); throw; } } public override void Remove(TEntity entity, Action persistMethod) { if (entity == null) throw new ArgumentNullException("entity"); if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { persistMethod(entity); } finally { //set the disposal action SetCacheAction(() => { //Clear all Cache.ClearCacheItem(GetCacheTypeKey()); }); } } public override TEntity Get(TId id, Func getFromRepo) { //Force get all with cache var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); //we don't have anything in cache (this should never happen), just return from the repo if (found == null) return getFromRepo(id); var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); if (entity == null) return null; //We must ensure to deep clone each one out manually since the deep clone list only clones one way return (TEntity)entity.DeepClone(); } public override TEntity Get(TId id) { //Force get all with cache var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); //we don't have anything in cache (this should never happen), just return null if (found == null) return null; var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); if (entity == null) return null; //We must ensure to deep clone each one out manually since the deep clone list only clones one way return (TEntity)entity.DeepClone(); } public override bool Exists(TId id, Func getFromRepo) { //Force get all with cache var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); //we don't have anything in cache (this should never happen), just return from the repo return found == null ? getFromRepo(id) : found.Any(x => _getEntityId(x).Equals(id)); } public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) { //process getting all including setting the cache callback var result = PerformGetAll(getFromRepo); //now that the base result has been calculated, they will all be cached. // Now we can just filter by ids if they have been supplied return (ids.Any() ? result.Where(x => ids.Contains(_getEntityId(x))).ToArray() : result) //We must ensure to deep clone each one out manually since the deep clone list only clones one way .Select(x => (TEntity)x.DeepClone()) .ToArray(); } private TEntity[] PerformGetAll(Func> getFromRepo) { var allEntities = GetAllFromCache(); if (allEntities.Any()) { return allEntities; } //check the zero count cache if (HasZeroCountCache()) { //there is a zero count cache so return an empty list return new TEntity[] { }; } //we need to do the lookup from the repo var entityCollection = getFromRepo(new TId[] { }) //ensure we don't include any null refs in the returned collection! .WhereNotNull() .ToArray(); //set the disposal action SetCacheAction(entityCollection); return entityCollection; } /// /// For this type of caching policy, we don't cache individual items /// /// /// protected void SetCacheAction(string cacheKey, TEntity entity) { //No-op } /// /// Sets the action to execute on disposal for an entity collection /// /// protected void SetCacheAction(TEntity[] entityCollection) { //set the disposal action SetCacheAction(() => { //We want to cache the result as a single collection if (_expires) { Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection), timeout: TimeSpan.FromMinutes(5), isSliding: true); } else { Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); } }); } /// /// Looks up the zero count cache, must return null if it doesn't exist /// /// protected bool HasZeroCountCache() { if (_hasZeroCountCache.HasValue) return _hasZeroCountCache.Value; _hasZeroCountCache = Cache.GetCacheItem>(GetCacheTypeKey()) != null; return _hasZeroCountCache.Value; } /// /// This policy will cache the full data set as a single collection /// /// protected TEntity[] GetAllFromCache() { var found = Cache.GetCacheItem>(GetCacheTypeKey()); //This method will get called before checking for zero count cache, so we'll just set the flag here _hasZeroCountCache = found != null; return found == null ? new TEntity[] { } : found.WhereNotNull().ToArray(); } } }