From 4ab7f768fabe4b8ae27e89f35a179956c558082e Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 18 Jan 2017 18:48:13 +0100 Subject: [PATCH] U4-9322 - backport repository cache policy changes from v8 --- .../Cache/DefaultRepositoryCachePolicy.cs | 310 ++++++++---------- .../DefaultRepositoryCachePolicyFactory.cs | 27 -- .../Cache/FullDataSetRepositoryCachePolicy.cs | 269 ++++++--------- ...FullDataSetRepositoryCachePolicyFactory.cs | 33 -- .../Cache/IRepositoryCachePolicy.cs | 84 ++++- .../Cache/IRepositoryCachePolicyFactory.cs | 9 - ...SingleItemsRepositoryCachePolicyFactory.cs | 27 -- .../Cache/RepositoryCachePolicyBase.cs | 58 ++-- .../Cache/RepositoryCachePolicyOptions.cs | 3 + .../SingleItemsOnlyRepositoryCachePolicy.cs | 25 +- .../Repositories/ContentTypeRepository.cs | 16 +- .../Repositories/DictionaryRepository.cs | 85 ++--- .../Repositories/DomainRepository.cs | 13 +- .../Repositories/LanguageRepository.cs | 13 +- .../Repositories/MediaTypeRepository.cs | 14 +- .../Repositories/MemberTypeRepository.cs | 16 +- .../Repositories/PetaPocoRepositoryBase.cs | 1 - .../Repositories/PublicAccessRepository.cs | 13 +- .../Repositories/RelationTypeRepository.cs | 15 +- .../Repositories/RepositoryBase.cs | 79 ++--- .../Repositories/TemplateRepository.cs | 12 +- src/Umbraco.Core/Umbraco.Core.csproj | 4 - .../Cache/DefaultCachePolicyTests.cs | 50 +-- .../Cache/FullDataSetCachePolicyTests.cs | 72 ++-- .../Cache/SingleItemsOnlyCachePolicyTests.cs | 18 +- 25 files changed, 544 insertions(+), 722 deletions(-) delete mode 100644 src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs delete mode 100644 src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs delete mode 100644 src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs delete mode 100644 src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs index 1f51fc3ccc..656d532d8a 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -1,23 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { /// - /// The default cache policy for retrieving a single entity + /// Represents the default cache policy. /// - /// - /// + /// The type of the entity. + /// The type of the identifier. /// - /// This cache policy uses sliding expiration and caches instances for 5 minutes. However if allow zero count is true, then we use the - /// default policy with no expiry. + /// 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. /// internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { + private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) @@ -27,242 +29,216 @@ namespace Umbraco.Core.Cache _options = options; } - protected string GetCacheIdKey(object id) + protected string GetEntityCacheKey(object id) { if (id == null) throw new ArgumentNullException("id"); - - return string.Format("{0}{1}", GetCacheTypeKey(), id); + return GetEntityTypeCacheKey() + id; } - protected string GetCacheTypeKey() + protected string GetEntityTypeCacheKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } - public override void CreateOrUpdate(TEntity entity, Action persistMethod) + protected virtual void InsertEntity(string cacheKey, TEntity entity) + { + Cache.InsertCacheItem(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.InsertCacheItem(GetEntityTypeCacheKey(), () => EmptyEntities); + } + else + { + // individually cache each item + foreach (var entity in entities) + { + var capture = entity; + Cache.InsertCacheItem(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); + } + } + } + + /// + public override void Create(TEntity entity, Action persistNew) { if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { - persistMethod(entity); + persistNew(entity); - //set the disposal action - SetCacheAction(() => + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - //just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity, - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); - + Cache.InsertCacheItem(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); + } + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); } catch { - //set the disposal action - SetCacheAction(() => - { - //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.ClearCacheItem(GetCacheIdKey(entity.Id)); + // 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.ClearCacheItem(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); - throw; } } - public override void Remove(TEntity entity, Action persistMethod) + /// + public override void Update(TEntity entity, Action persistUpdated) { if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { - persistMethod(entity); - } - finally - { - //set the disposal action - var cacheKey = GetCacheIdKey(entity.Id); - SetCacheAction(() => + persistUpdated(entity); + + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - Cache.ClearCacheItem(cacheKey); - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + Cache.InsertCacheItem(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); + } + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + } + 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.ClearCacheItem(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + + throw; } } - public override TEntity Get(TId id, Func getFromRepo) + /// + public override void Delete(TEntity entity, Action persistDeleted) { - if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + if (entity == null) throw new ArgumentNullException("entity"); - var cacheKey = GetCacheIdKey(id); + try + { + persistDeleted(entity); + } + finally + { + // whatever happens, clear the cache + var cacheKey = GetEntityCacheKey(entity.Id); + Cache.ClearCacheItem(cacheKey); + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetEntityTypeCacheKey()); + } + } + + /// + 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 = getFromRepo(id); + var entity = performGet(id); - //set the disposal action - SetCacheAction(cacheKey, entity); + if (entity != null && entity.HasIdentity) + InsertEntity(cacheKey, entity); return entity; } - public override TEntity Get(TId id) + /// + public override TEntity GetCached(TId id) { - var cacheKey = GetCacheIdKey(id); + var cacheKey = GetEntityCacheKey(id); return Cache.GetCacheItem(cacheKey); } - public override bool Exists(TId id, Func getFromRepo) + /// + public override bool Exists(TId id, Func performExists, Func> performGetAll) { - if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); - - var cacheKey = GetCacheIdKey(id); + // if found in cache the return else check + var cacheKey = GetEntityCacheKey(id); var fromCache = Cache.GetCacheItem(cacheKey); - return fromCache != null || getFromRepo(id); + return fromCache != null || performExists(id); } - public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + /// + public override TEntity[] GetAll(TId[] ids, Func> performGetAll) { - if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); - - if (ids.Any()) + if (ids.Length > 0) { - var entities = ids.Select(Get).ToArray(); - if (ids.Length.Equals(entities.Length) && entities.Any(x => x == null) == false) - return entities; + // 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 { - var allEntities = GetAllFromCache(); - if (allEntities.Any()) + // get everything we have + var entities = Cache.GetCacheItemsByKeySearch(GetEntityTypeCacheKey()) + .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) { - //Get count of all entities of current type (TEntity) to ensure cached result is correct + // need to validate the count, get the actual count and return if ok var totalCount = _options.PerformCount(); - if (allEntities.Length == totalCount) - return allEntities; + if (entities.Length == totalCount) + return entities; } else { - return allEntities; + // no need to validate, just return what we have and assume it's all there is + return entities; } } else if (_options.GetAllCacheAllowZeroCount) { - //if the repository allows caching a zero count, then check the zero count cache - if (HasZeroCountCache()) - { - //there is a zero count cache so return an empty list - return new TEntity[] {}; - } + // if none of them were in the cache + // and we allow zero count - check for the special (empty) entry + var empty = Cache.GetCacheItem(GetEntityTypeCacheKey()); + if (empty != null) return empty; } } - //we need to do the lookup from the repo - var entityCollection = getFromRepo(ids) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull() + // 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(); - //set the disposal action - SetCacheAction(ids, entityCollection); + // note: if empty & allow zero count, will cache a special (empty) entry + InsertEntities(ids, repoEntities); - return entityCollection; + return repoEntities; } - /// - /// Looks up the zero count cache, must return null if it doesn't exist - /// - /// - protected bool HasZeroCountCache() + /// + public override void ClearAll() { - var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); - return (zeroCount != null && zeroCount.Any() == false); + Cache.ClearCacheByKeySearch(GetEntityTypeCacheKey()); } - - /// - /// Performs the lookup for all entities of this type from the cache - /// - /// - protected TEntity[] GetAllFromCache() - { - var allEntities = Cache.GetCacheItemsByKeySearch(GetCacheTypeKey()) - .WhereNotNull() - .ToArray(); - return allEntities.Any() ? allEntities : new TEntity[] {}; - } - - /// - /// Sets the action to execute on disposal for a single entity - /// - /// - /// - protected virtual void SetCacheAction(string cacheKey, TEntity entity) - { - if (entity == null) return; - - SetCacheAction(() => - { - //just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.InsertCacheItem(cacheKey, () => entity, - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - }); - } - - /// - /// Sets the action to execute on disposal for an entity collection - /// - /// - /// - protected virtual void SetCacheAction(TId[] ids, TEntity[] entityCollection) - { - SetCacheAction(() => - { - //This option cannot execute if we are looking up specific Ids - if (ids.Any() == false && entityCollection.Length == 0 && _options.GetAllCacheAllowZeroCount) - { - //there was nothing returned but we want to cache a zero count result so add an TEntity[] to the cache - // to signify that there is a zero count cache - //NOTE: Don't set expiry/sliding for a zero count - Cache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); - } - else - { - //This is the default behavior, we'll individually cache each item so that if/when these items are resolved - // by id, they are returned from the already existing cache. - foreach (var entity in entityCollection.WhereNotNull()) - { - var localCopy = entity; - //just to be safe, we cannot cache an item without an identity - if (localCopy.HasIdentity) - { - Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy, - timeout: TimeSpan.FromMinutes(5), - isSliding: true); - } - } - } - }); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs deleted file mode 100644 index 5c02e41a48..0000000000 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - /// - /// Creates cache policies - /// - /// - /// - internal class DefaultRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory - where TEntity : class, IAggregateRoot - { - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly RepositoryCachePolicyOptions _options; - - public DefaultRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) - { - _runtimeCache = runtimeCache; - _options = options; - } - - public virtual IRepositoryCachePolicy CreatePolicy() - { - return new DefaultRepositoryCachePolicy(_runtimeCache, _options); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index 9b37d1861f..95ad7e2bf0 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -7,223 +7,168 @@ using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { /// - /// A caching policy that caches an entire dataset as a single collection + /// 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, IAggregateRoot { - private readonly Func _getEntityId; - private readonly Func> _getAllFromRepo; + private readonly Func _entityGetId; private readonly bool _expires; - public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId, Func> getAllFromRepo, bool expires) + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func entityGetId, bool expires) : base(cache) { - _getEntityId = getEntityId; - _getAllFromRepo = getAllFromRepo; + _entityGetId = entityGetId; _expires = expires; } - private bool? _hasZeroCountCache; + protected static readonly TId[] EmptyIds = new TId[0]; // const - - protected string GetCacheTypeKey() + protected string GetEntityTypeCacheKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } - public override void CreateOrUpdate(TEntity entity, Action persistMethod) + protected void InsertEntities(TEntity[] entities) { - if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + // 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. - try + if (_expires) { - persistMethod(entity); - - //set the disposal action - SetCacheAction(() => - { - //Clear all - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + Cache.InsertCacheItem(GetEntityTypeCacheKey(), () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); } - catch + else { - //set the disposal action - SetCacheAction(() => - { - //Clear all - Cache.ClearCacheItem(GetCacheTypeKey()); - }); - throw; + Cache.InsertCacheItem(GetEntityTypeCacheKey(), () => new DeepCloneableList(entities)); } } - public override void Remove(TEntity entity, Action persistMethod) + /// + public override void Create(TEntity entity, Action persistNew) { if (entity == null) throw new ArgumentNullException("entity"); - if (persistMethod == null) throw new ArgumentNullException("persistMethod"); try { - persistMethod(entity); + persistNew(entity); } finally { - //set the disposal action - SetCacheAction(() => - { - //Clear all - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + ClearAll(); } } - public override TEntity Get(TId id, Func getFromRepo) + /// + public override void Update(TEntity entity, Action persistUpdated) { - //Force get all with cache - var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + if (entity == null) throw new ArgumentNullException("entity"); - //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()) + try { - return allEntities; + persistUpdated(entity); } - - //check the zero count cache - if (HasZeroCountCache()) + finally { - //there is a zero count cache so return an empty list - return new TEntity[] { }; + ClearAll(); } - - //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) + /// + public override void Delete(TEntity entity, Action persistDeleted) { - //No-op - } + if (entity == null) throw new ArgumentNullException("entity"); - /// - /// Sets the action to execute on disposal for an entity collection - /// - /// - protected void SetCacheAction(TEntity[] entityCollection) - { - //set the disposal action - SetCacheAction(() => + try { - //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)); - } - }); + persistDeleted(entity); + } + finally + { + ClearAll(); + } } - /// - /// Looks up the zero count cache, must return null if it doesn't exist - /// - /// - protected bool HasZeroCountCache() + /// + public override TEntity Get(TId id, Func performGet, Func> performGetAll) { - if (_hasZeroCountCache.HasValue) - return _hasZeroCountCache.Value; + // get all from the cache, then look for the entity + var all = GetAllCached(performGetAll); + var entity = all.FirstOrDefault(x => _entityGetId(x).Equals(id)); - _hasZeroCountCache = Cache.GetCacheItem>(GetCacheTypeKey()) != null; - return _hasZeroCountCache.Value; + // 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 entity == null ? null : (TEntity) entity.DeepClone(); } - /// - /// This policy will cache the full data set as a single collection - /// - /// - protected TEntity[] GetAllFromCache() + /// + public override TEntity GetCached(TId id) { - var found = Cache.GetCacheItem>(GetCacheTypeKey()); + // get all from the cache -- and only the cache, then look for the entity + var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + var entity = all == null ? null : all.FirstOrDefault(x => _entityGetId(x).Equals(id)); - //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(); + // 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 entity == null ? null : (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()); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs deleted file mode 100644 index e4addcf355..0000000000 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - /// - /// Creates cache policies - /// - /// - /// - internal class FullDataSetRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory - where TEntity : class, IAggregateRoot - { - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly Func _getEntityId; - private readonly Func> _getAllFromRepo; - private readonly bool _expires; - - public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, Func getEntityId, Func> getAllFromRepo, bool expires) - { - _runtimeCache = runtimeCache; - _getEntityId = getEntityId; - _getAllFromRepo = getAllFromRepo; - _expires = expires; - } - - public virtual IRepositoryCachePolicy CreatePolicy() - { - return new FullDataSetRepositoryCachePolicy(_runtimeCache, _getEntityId, _getAllFromRepo, _expires); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs index 215487c3be..11a3cf6bb9 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -4,15 +4,83 @@ using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { - internal interface IRepositoryCachePolicy : IDisposable + internal interface IRepositoryCachePolicy where TEntity : class, IAggregateRoot { - TEntity Get(TId id, Func getFromRepo); - TEntity Get(TId id); - bool Exists(TId id, Func getFromRepo); - - void CreateOrUpdate(TEntity entity, Action persistMethod); - void Remove(TEntity entity, Action persistMethod); - TEntity[] GetAll(TId[] ids, Func> getFromRepo); + // note: + // at the moment each repository instance creates its corresponding cache policy instance + // we could reduce allocations by using static cache policy instances but then we would need + // to modify all methods here to pass the repository and cache eg: + // + // TEntity Get(TRepository repository, IRuntimeCacheProvider cache, TId id); + // + // it is not *that* complicated but then RepositoryBase needs to have a TRepository generic + // type parameter and it all becomes convoluted - keeping it simple for the time being. + + /// + /// Gets an entity from the cache, else from the repository. + /// + /// The identifier. + /// The repository PerformGet method. + /// The repository PerformGetAll method. + /// The entity with the specified identifier, if it exits, else null. + /// First considers the cache then the repository. + TEntity Get(TId id, Func performGet, Func> performGetAll); + + /// + /// Gets an entity from the cache. + /// + /// The identifier. + /// The entity with the specified identifier, if it is in the cache already, else null. + /// Does not consider the repository at all. + TEntity GetCached(TId id); + + /// + /// Gets a value indicating whether an entity with a specified identifier exists. + /// + /// The identifier. + /// The repository PerformExists method. + /// The repository PerformGetAll method. + /// A value indicating whether an entity with the specified identifier exists. + /// First considers the cache then the repository. + bool Exists(TId id, Func performExists, Func> performGetAll); + + /// + /// Creates an entity. + /// + /// The entity. + /// The repository PersistNewItem method. + /// Creates the entity in the repository, and updates the cache accordingly. + void Create(TEntity entity, Action persistNew); + + /// + /// Updates an entity. + /// + /// The entity. + /// The reopsitory PersistUpdatedItem method. + /// Updates the entity in the repository, and updates the cache accordingly. + void Update(TEntity entity, Action persistUpdated); + + /// + /// Removes an entity. + /// + /// The entity. + /// The repository PersistDeletedItem method. + /// Removes the entity from the repository and clears the cache. + void Delete(TEntity entity, Action persistDeleted); + + /// + /// Gets entities. + /// + /// The identifiers. + /// The repository PerformGetAll method. + /// If is empty, all entities, else the entities with the specified identifiers. + /// Get all the entities. Either from the cache or the repository depending on the implementation. + TEntity[] GetAll(TId[] ids, Func> performGetAll); + + /// + /// Clears the entire cache. + /// + void ClearAll(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs deleted file mode 100644 index 2d69704b63..0000000000 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - internal interface IRepositoryCachePolicyFactory where TEntity : class, IAggregateRoot - { - IRepositoryCachePolicy CreatePolicy(); - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs deleted file mode 100644 index b24838bc3b..0000000000 --- a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Umbraco.Core.Models.EntityBase; - -namespace Umbraco.Core.Cache -{ - /// - /// Creates cache policies - /// - /// - /// - internal class OnlySingleItemsRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory - where TEntity : class, IAggregateRoot - { - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly RepositoryCachePolicyOptions _options; - - public OnlySingleItemsRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) - { - _runtimeCache = runtimeCache; - _options = options; - } - - public virtual IRepositoryCachePolicy CreatePolicy() - { - return new SingleItemsOnlyRepositoryCachePolicy(_runtimeCache, _options); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs index b939cd14e6..ce366e3d0b 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs @@ -4,45 +4,45 @@ using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { - internal abstract class RepositoryCachePolicyBase : DisposableObject, IRepositoryCachePolicy + /// + /// A base class for repository cache policies. + /// + /// The type of the entity. + /// The type of the identifier. + internal abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy where TEntity : class, IAggregateRoot { - private Action _action; - protected RepositoryCachePolicyBase(IRuntimeCacheProvider cache) { - if (cache == null) throw new ArgumentNullException("cache"); - + if (cache == null) throw new ArgumentNullException("cache"); Cache = cache; } protected IRuntimeCacheProvider Cache { get; private set; } - /// - /// The disposal performs the caching - /// - protected override void DisposeResources() - { - if (_action != null) - { - _action(); - } - } + /// + public abstract TEntity Get(TId id, Func performGet, Func> performGetAll); - /// - /// Sets the action to execute on disposal - /// - /// - protected void SetCacheAction(Action action) - { - _action = action; - } + /// + public abstract TEntity GetCached(TId id); + + /// + public abstract bool Exists(TId id, Func performExists, Func> performGetAll); + + /// + public abstract void Create(TEntity entity, Action persistNew); + + /// + public abstract void Update(TEntity entity, Action persistUpdated); + + /// + public abstract void Delete(TEntity entity, Action persistDeleted); + + /// + public abstract TEntity[] GetAll(TId[] ids, Func> performGetAll); + + /// + public abstract void ClearAll(); - public abstract TEntity Get(TId id, Func getFromRepo); - public abstract TEntity Get(TId id); - public abstract bool Exists(TId id, Func getFromRepo); - public abstract void CreateOrUpdate(TEntity entity, Action persistMethod); - public abstract void Remove(TEntity entity, Action persistMethod); - public abstract TEntity[] GetAll(TId[] ids, Func> getFromRepo); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index e8c6ac02b0..14cef76db6 100644 --- a/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -2,6 +2,9 @@ using System; namespace Umbraco.Core.Cache { + /// + /// Specifies how a repository cache policy should cache entities. + /// internal class RepositoryCachePolicyOptions { /// diff --git a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index 28ac4ee2d1..7ba7d445fe 100644 --- a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -1,24 +1,27 @@ -using System.Linq; -using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { /// - /// A caching policy that ignores all caches for GetAll - it will only cache calls for individual items + /// Represents a special policy that does not cache the result of GetAll. /// - /// - /// + /// The type of the entity. + /// The type of the identifier. + /// + /// Overrides the default repository cache policy and does not writes the result of GetAll + /// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. + /// Used by DictionaryRepository. + /// internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy where TEntity : class, IAggregateRoot { - public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache, options) + public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) + : base(cache, options) + { } + + protected override void InsertEntities(TId[] ids, TEntity[] entities) { - } - - protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) - { - //no-op + // nop } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index b7b4ddd583..ca53b2e04e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -2,19 +2,13 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; - -using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -24,6 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories internal class ContentTypeRepository : ContentTypeBaseRepository, IContentTypeRepository { private readonly ITemplateRepository _templateRepository; + private IRepositoryCachePolicy _cachePolicy; public ContentTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, ITemplateRepository templateRepository) : base(work, cache, logger, sqlSyntax) @@ -31,16 +26,11 @@ namespace Umbraco.Core.Persistence.Repositories _templateRepository = templateRepository; } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), - //allow this cache to expire - expires:true)); + return _cachePolicy ?? (_cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index 971efc4f2d..da6d4d94a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -20,24 +20,27 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DictionaryRepository : PetaPocoRepositoryBase, IDictionaryRepository { + private IRepositoryCachePolicy _cachePolicy; + public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax) : base(work, cache, logger, syntax) - { - } + { } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //custom cache policy which will not cache any results for GetAll - return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - })); + if (_cachePolicy != null) return _cachePolicy; + + var options = new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; + + _cachePolicy = new SingleItemsOnlyRepositoryCachePolicy(RuntimeCache, options); + + return _cachePolicy; } } @@ -52,7 +55,7 @@ namespace Umbraco.Core.Persistence.Repositories var dto = Database.Fetch(new DictionaryLanguageTextRelator().Map, sql).FirstOrDefault(); if (dto == null) return null; - + var entity = ConvertFromDto(dto); //on initial construction we don't want to have dirty properties tracked @@ -80,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); sql.OrderBy(x => x.UniqueId, SqlSyntax); - + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) .Select(x => ConvertFromDto(x)); } @@ -149,7 +152,7 @@ namespace Umbraco.Core.Persistence.Repositories translation.Key = dictionaryItem.Key; } - dictionaryItem.ResetDirtyProperties(); + dictionaryItem.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IDictionaryItem entity) @@ -223,7 +226,7 @@ namespace Umbraco.Core.Persistence.Repositories var list = new List(); foreach (var textDto in dto.LanguageTextDtos) - { + { if (textDto.LanguageId <= 0) continue; @@ -250,7 +253,7 @@ namespace Umbraco.Core.Persistence.Repositories return keyRepo.Get(key); } } - + private IEnumerable GetRootDictionaryItems() { var query = Query.Builder.Where(x => x.ParentId == null); @@ -291,6 +294,7 @@ namespace Umbraco.Core.Persistence.Repositories private class DictionaryByUniqueIdRepository : SimpleGetRepository { + private IRepositoryCachePolicy _cachePolicy; private readonly DictionaryRepository _dictionaryRepository; public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) @@ -329,25 +333,28 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //custom cache policy which will not cache any results for GetAll - return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - })); + if (_cachePolicy != null) return _cachePolicy; + + var options = new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; + + _cachePolicy = new SingleItemsOnlyRepositoryCachePolicy(RuntimeCache, options); + + return _cachePolicy; } } } private class DictionaryByKeyRepository : SimpleGetRepository { + private IRepositoryCachePolicy _cachePolicy; private readonly DictionaryRepository _dictionaryRepository; public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) @@ -386,23 +393,23 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //custom cache policy which will not cache any results for GetAll - return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - })); + if (_cachePolicy != null) return _cachePolicy; + + var options = new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; + + _cachePolicy = new SingleItemsOnlyRepositoryCachePolicy(RuntimeCache, options); + + return _cachePolicy; } } } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 7b6cc162a8..682c75eeb6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -18,19 +18,22 @@ namespace Umbraco.Core.Persistence.Repositories internal class DomainRepository : PetaPocoRepositoryBase, IDomainRepository { + private IRepositoryCachePolicy _cachePolicy; + public DomainRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); + + return _cachePolicy; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index f9a8e59cfa..7911df8edb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -19,19 +19,22 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class LanguageRepository : PetaPocoRepositoryBase, ILanguageRepository { + private IRepositoryCachePolicy _cachePolicy; + public LanguageRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); + + return _cachePolicy; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 50a89bfd65..516c08330a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -22,22 +22,22 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class MediaTypeRepository : ContentTypeBaseRepository, IMediaTypeRepository { + private IRepositoryCachePolicy _cachePolicy; public MediaTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), - //allow this cache to expire - expires: true)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); + + return _cachePolicy; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 7d5defe8c9..fd26afac89 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -21,25 +21,25 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class MemberTypeRepository : ContentTypeBaseRepository, IMemberTypeRepository { + private IRepositoryCachePolicy _cachePolicy; public MemberTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), - //allow this cache to expire - expires: true)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); + + return _cachePolicy; } } - + protected override IMemberType PerformGet(int id) { //use the underlying GetAll which will force cache all content types diff --git a/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs index fe363fea16..c02316e7f4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PetaPocoRepositoryBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data.SqlServerCe; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 37200b2172..c34952f4be 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -15,18 +15,21 @@ namespace Umbraco.Core.Persistence.Repositories { internal class PublicAccessRepository : PetaPocoRepositoryBase, IPublicAccessRepository { + private IRepositoryCachePolicy _cachePolicy; + public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); + + return _cachePolicy; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs index 2c54897ff7..148ebba456 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs @@ -19,20 +19,21 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class RelationTypeRepository : PetaPocoRepositoryBase, IRelationTypeRepository { + private IRepositoryCachePolicy _cachePolicy; + public RelationTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { } - // assuming we don't have tons of relation types, use a FullDataSet policy, ie - // cache the entire GetAll result once in a single collection - which can expire - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - return _cachePolicyFactory - ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), expires: true)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ true); + + return _cachePolicy; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 41946d48d4..201520cbe3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -82,7 +82,6 @@ namespace Umbraco.Core.Persistence.Repositories { } - #region Static Queries private IQuery _hasIdQuery; @@ -102,30 +101,25 @@ namespace Umbraco.Core.Persistence.Repositories get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache(); } } - private IRepositoryCachePolicyFactory _cachePolicyFactory; - /// - /// Returns the Cache Policy for the repository - /// - /// - /// The Cache Policy determines how each entity or entity collection is cached - /// - protected virtual IRepositoryCachePolicyFactory CachePolicyFactory + private IRepositoryCachePolicy _cachePolicy; + + protected virtual IRepositoryCachePolicy CachePolicy { get { - return _cachePolicyFactory ?? (_cachePolicyFactory = new DefaultRepositoryCachePolicyFactory( - RuntimeCache, - new RepositoryCachePolicyOptions(() => - { - //create it once if it is needed (no need for locking here) - if (_hasIdQuery == null) - { - _hasIdQuery = Query.Builder.Where(x => x.Id != 0); - } + if (_cachePolicy != null) return _cachePolicy; - //Get count of all entities of current type (TEntity) to ensure cached result is correct - return PerformCount(_hasIdQuery); - }))); + var options = new RepositoryCachePolicyOptions(() => + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + //create query once if it is needed (no need for locking here) + var query = _hasIdQuery ?? (_hasIdQuery = Query.Builder.Where(x => x.Id != 0)); + return PerformCount(query); + }); + + _cachePolicy = new DefaultRepositoryCachePolicy(RuntimeCache, options); + + return _cachePolicy; } } @@ -166,10 +160,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public TEntity Get(TId id) { - using (var p = CachePolicyFactory.CreatePolicy()) - { - return p.Get(id, PerformGet); - } + return CachePolicy.Get(id, PerformGet, PerformGetAll); } protected abstract IEnumerable PerformGetAll(params TId[] ids); @@ -192,13 +183,9 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); } - using (var p = CachePolicyFactory.CreatePolicy()) - { - var result = p.GetAll(ids, PerformGetAll); - return result; - } + return CachePolicy.GetAll(ids, PerformGetAll); } - + protected abstract IEnumerable PerformGetByQuery(IQuery query); /// /// Gets a list of entities by the passed in query @@ -220,10 +207,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public bool Exists(TId id) { - using (var p = CachePolicyFactory.CreatePolicy()) - { - return p.Exists(id, PerformExists); - } + return CachePolicy.Exists(id, PerformExists, PerformGetAll); } protected abstract int PerformCount(IQuery query); @@ -236,19 +220,14 @@ namespace Umbraco.Core.Persistence.Repositories { return PerformCount(query); } - + /// /// Unit of work method that tells the repository to persist the new entity /// /// public virtual void PersistNewItem(IEntity entity) { - var casted = (TEntity)entity; - - using (var p = CachePolicyFactory.CreatePolicy()) - { - p.CreateOrUpdate(casted, PersistNewItem); - } + CachePolicy.Create((TEntity) entity, PersistNewItem); } /// @@ -257,12 +236,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistUpdatedItem(IEntity entity) { - var casted = (TEntity)entity; - - using (var p = CachePolicyFactory.CreatePolicy()) - { - p.CreateOrUpdate(casted, PersistUpdatedItem); - } + CachePolicy.Update((TEntity) entity, PersistUpdatedItem); } /// @@ -271,20 +245,13 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistDeletedItem(IEntity entity) { - var casted = (TEntity)entity; - - using (var p = CachePolicyFactory.CreatePolicy()) - { - p.Remove(casted, PersistDeletedItem); - } + CachePolicy.Delete((TEntity) entity, PersistDeletedItem); } - protected abstract void PersistNewItem(TEntity item); protected abstract void PersistUpdatedItem(TEntity item); protected abstract void PersistDeletedItem(TEntity item); - /// /// Dispose disposable properties /// diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index 41743601fb..a164e7e5ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -33,6 +33,7 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITemplatesSection _templateConfig; private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; + private IRepositoryCachePolicy _cachePolicy; internal TemplateRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) : base(work, cache, logger, sqlSyntax) @@ -45,14 +46,15 @@ namespace Umbraco.Core.Persistence.Repositories } - private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; - protected override IRepositoryCachePolicyFactory CachePolicyFactory + protected override IRepositoryCachePolicy CachePolicy { get { - //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( - RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + if (_cachePolicy != null) return _cachePolicy; + + _cachePolicy = new FullDataSetRepositoryCachePolicy(RuntimeCache, GetEntityId, /*expires:*/ false); + + return _cachePolicy; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index afa85609d0..fc778bc53b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -136,9 +136,7 @@ - - @@ -146,7 +144,6 @@ - @@ -156,7 +153,6 @@ - diff --git a/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs b/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs index 9b0aaac78b..83b0db237f 100644 --- a/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs @@ -24,10 +24,8 @@ namespace Umbraco.Tests.Cache }); var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); - using (defaultPolicy) - { - var found = defaultPolicy.Get(1, o => new AuditItem(1, "blah", AuditType.Copy, 123)); - } + + var found = defaultPolicy.Get(1, id => new AuditItem(1, "blah", AuditType.Copy, 123), o => null); Assert.IsTrue(isCached); } @@ -38,11 +36,9 @@ namespace Umbraco.Tests.Cache cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem(1, "blah", AuditType.Copy, 123)); var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); - using (defaultPolicy) - { - var found = defaultPolicy.Get(1, o => (AuditItem) null); - Assert.IsNotNull(found); - } + + var found = defaultPolicy.Get(1, id => null, ids => null); + Assert.IsNotNull(found); } [Test] @@ -59,14 +55,12 @@ namespace Umbraco.Tests.Cache cache.Setup(x => x.GetCacheItemsByKeySearch(It.IsAny())).Returns(new AuditItem[] {}); var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); - using (defaultPolicy) + + var found = defaultPolicy.GetAll(new object[] { }, ids => new[] { - var found = defaultPolicy.GetAll(new object[] {}, o => new[] - { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) - }); - } + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }); Assert.AreEqual(2, cached.Count); } @@ -82,11 +76,9 @@ namespace Umbraco.Tests.Cache }); var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); - using (defaultPolicy) - { - var found = defaultPolicy.GetAll(new object[] {}, o => new[] {(AuditItem) null}); - Assert.AreEqual(2, found.Length); - } + + var found = defaultPolicy.GetAll(new object[] { }, ids => new[] { (AuditItem)null }); + Assert.AreEqual(2, found.Length); } [Test] @@ -103,13 +95,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); try { - using (defaultPolicy) - { - defaultPolicy.CreateOrUpdate(new AuditItem(1, "blah", AuditType.Copy, 123), item => - { - throw new Exception("blah!"); - }); - } + defaultPolicy.Update(new AuditItem(1, "blah", AuditType.Copy, 123), item => { throw new Exception("blah!"); }); } catch { @@ -135,13 +121,7 @@ namespace Umbraco.Tests.Cache var defaultPolicy = new DefaultRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); try { - using (defaultPolicy) - { - defaultPolicy.Remove(new AuditItem(1, "blah", AuditType.Copy, 123), item => - { - throw new Exception("blah!"); - }); - } + defaultPolicy.Delete(new AuditItem(1, "blah", AuditType.Copy, 123), item => { throw new Exception("blah!"); }); } catch { diff --git a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs index 96e22e3aff..ee519afb76 100644 --- a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs @@ -32,11 +32,9 @@ namespace Umbraco.Tests.Cache isCached = true; }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); - using (defaultPolicy) - { - var found = defaultPolicy.Get(1, o => new AuditItem(1, "blah", AuditType.Copy, 123)); - } + var policy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); + + var found = policy.Get(1, id => new AuditItem(1, "blah", AuditType.Copy, 123), ids => getAll); Assert.IsTrue(isCached); } @@ -52,12 +50,10 @@ namespace Umbraco.Tests.Cache var cache = new Mock(); cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem(1, "blah", AuditType.Copy, 123)); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); - using (defaultPolicy) - { - var found = defaultPolicy.Get(1, o => (AuditItem)null); - Assert.IsNotNull(found); - } + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); + + var found = defaultPolicy.Get(1, id => null, ids => getAll); + Assert.IsNotNull(found); } [Test] @@ -84,21 +80,17 @@ namespace Umbraco.Tests.Cache return cached.Any() ? new DeepCloneableList(ListCloneBehavior.CloneOnce) : null; }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); - using (defaultPolicy) - { - var found = defaultPolicy.GetAll(new object[] {}, o => getAll); - } + var policy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); + + var found = policy.GetAll(new object[] { }, ids => getAll); Assert.AreEqual(1, cached.Count); Assert.IsNotNull(list); //Do it again, ensure that its coming from the cache! - defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); - using (defaultPolicy) - { - var found = defaultPolicy.GetAll(new object[] { }, o => getAll); - } + policy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); + + found = policy.GetAll(new object[] { }, ids => getAll); Assert.AreEqual(1, cached.Count); Assert.IsNotNull(list); @@ -127,11 +119,9 @@ namespace Umbraco.Tests.Cache }); cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem[] { }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); - using (defaultPolicy) - { - var found = defaultPolicy.GetAll(new object[] { }, o => getAll); - } + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); + + var found = defaultPolicy.GetAll(new object[] { }, ids => getAll); Assert.AreEqual(1, cached.Count); Assert.IsNotNull(list); @@ -150,12 +140,10 @@ namespace Umbraco.Tests.Cache new AuditItem(2, "blah2", AuditType.Copy, 123) }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); - using (defaultPolicy) - { - var found = defaultPolicy.GetAll(new object[] { }, o => getAll); - Assert.AreEqual(2, found.Length); - } + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); + + var found = defaultPolicy.GetAll(new object[] { }, ids => getAll); + Assert.AreEqual(2, found.Length); } [Test] @@ -175,16 +163,10 @@ namespace Umbraco.Tests.Cache cacheCleared = true; }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); try { - using (defaultPolicy) - { - defaultPolicy.CreateOrUpdate(new AuditItem(1, "blah", AuditType.Copy, 123), item => - { - throw new Exception("blah!"); - }); - } + defaultPolicy.Update(new AuditItem(1, "blah", AuditType.Copy, 123), item => { throw new Exception("blah!"); }); } catch { @@ -213,16 +195,10 @@ namespace Umbraco.Tests.Cache cacheCleared = true; }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, false); try { - using (defaultPolicy) - { - defaultPolicy.Remove(new AuditItem(1, "blah", AuditType.Copy, 123), item => - { - throw new Exception("blah!"); - }); - } + defaultPolicy.Delete(new AuditItem(1, "blah", AuditType.Copy, 123), item => { throw new Exception("blah!"); }); } catch { diff --git a/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs b/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs index b8e77e9267..55a7f2a893 100644 --- a/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/SingleItemsOnlyCachePolicyTests.cs @@ -25,14 +25,12 @@ namespace Umbraco.Tests.Cache cache.Setup(x => x.GetCacheItemsByKeySearch(It.IsAny())).Returns(new AuditItem[] { }); var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); - using (defaultPolicy) + + var found = defaultPolicy.GetAll(new object[] { }, ids => new[] { - var found = defaultPolicy.GetAll(new object[] { }, o => new[] - { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) - }); - } + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }); Assert.AreEqual(0, cached.Count); } @@ -50,10 +48,8 @@ namespace Umbraco.Tests.Cache }); var defaultPolicy = new SingleItemsOnlyRepositoryCachePolicy(cache.Object, new RepositoryCachePolicyOptions()); - using (defaultPolicy) - { - var found = defaultPolicy.Get(1, o => new AuditItem(1, "blah", AuditType.Copy, 123)); - } + + var found = defaultPolicy.Get(1, id => new AuditItem(1, "blah", AuditType.Copy, 123), ids => null); Assert.IsTrue(isCached); } }