diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs new file mode 100644 index 0000000000..c887af6ef0 --- /dev/null +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// The default cache policy for retrieving a single entity + /// + /// + /// + internal class DefaultRepositoryCachePolicy : DisposableObject, IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private readonly RepositoryCachePolicyOptions _options; + protected IRuntimeCacheProvider Cache { get; private set; } + private Action _action; + + public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) + { + _options = options; + Cache = cache; + } + + public string GetCacheIdKey(object id) + { + return string.Format("{0}{1}", GetCacheTypeKey(), id); + } + + public string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(TEntity).Name); + } + + public void CreateOrUpdate(TEntity entity, Action persistMethod) + { + var cacheKey = GetCacheIdKey(entity.Id); + + try + { + persistMethod(entity); + + //set the disposal action + SetCacheAction(() => Cache.InsertCacheItem(cacheKey, () => entity)); + + ////If there's a GetAll zero count cache, ensure it is cleared + //_cache.ClearCacheItem(GetCacheTypeKey()); + } + 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(cacheKey); + + ////If there's a GetAll zero count cache, ensure it is cleared + //_cache.ClearCacheItem(GetCacheTypeKey()); + throw; + } + } + + public void Remove(TEntity entity, Action persistMethod) + { + persistMethod(entity); + + //set the disposal action + var cacheKey = GetCacheIdKey(entity.Id); + SetCacheAction(() => Cache.ClearCacheItem(cacheKey)); + + ////If there's a GetAll zero count cache, ensure it is cleared + //_cache.ClearCacheItem(GetCacheTypeKey()); + } + + public TEntity Get(TId id, Func getFromRepo) + { + var cacheKey = GetCacheIdKey(id); + var fromCache = Cache.GetCacheItem(cacheKey); + if (fromCache != null) + return fromCache; + + var entity = getFromRepo(id); + + //set the disposal action + SetCacheAction(cacheKey, entity); + + return entity; + } + + public TEntity Get(TId id) + { + var cacheKey = GetCacheIdKey(id); + return Cache.GetCacheItem(cacheKey); + } + + public bool Exists(TId id, Func getFromRepo) + { + var cacheKey = GetCacheIdKey(id); + var fromCache = Cache.GetCacheItem(cacheKey); + return fromCache != null || getFromRepo(id); + } + + public virtual TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + if (ids.Any()) + { + var entities = ids.Select(Get).ToArray(); + if (ids.Length.Equals(entities.Length) && entities.Any(x => x == null) == false) + return entities; + } + else + { + var allEntities = GetAllFromCache(); + if (allEntities.Any()) + { + if (_options.GetAllCacheValidateCount) + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + var totalCount = _options.PerformCount(); + if (allEntities.Length == totalCount) + return allEntities; + } + else + { + return allEntities; + } + } + else if (_options.GetAllCacheAllowZeroCount) + { + //if the repository allows caching a zero count, then check the zero count cache + var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); + if (zeroCount != null && zeroCount.Any() == false) + { + //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(ids) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull() + .ToArray(); + + //set the disposal action + SetCacheAction(ids, entityCollection); + + return entityCollection; + } + + /// + /// Performs the lookup for all entities of this type from the cache + /// + /// + protected virtual TEntity[] GetAllFromCache() + { + var allEntities = Cache.GetCacheItemsByKeySearch(GetCacheTypeKey()) + .WhereNotNull() + .ToArray(); + return allEntities.Any() ? allEntities : new TEntity[] {}; + } + + /// + /// The disposal performs the caching + /// + protected override void DisposeResources() + { + if (_action != null) + { + _action(); + } + } + + /// + /// Sets the action to execute on disposal for a single entity + /// + /// + /// + protected virtual void SetCacheAction(string cacheKey, TEntity entity) + { + SetCacheAction(() => Cache.InsertCacheItem(cacheKey, () => entity)); + } + + /// + /// 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 + 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; + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy); + } + } + }); + } + + /// + /// Sets the action to execute on disposal + /// + /// + protected void SetCacheAction(Action action) + { + _action = action; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..5c02e41a48 --- /dev/null +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000000..c4c86b2ec7 --- /dev/null +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -0,0 +1,57 @@ +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 : DefaultRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache) : base(cache, new RepositoryCachePolicyOptions()) + { + } + + /// + /// For this type of caching policy, we don't cache individual items + /// + /// + /// + protected override void SetCacheAction(string cacheKey, TEntity entity) + { + //do nothing + } + + /// + /// Sets the action to execute on disposal for an entity collection + /// + /// + /// + protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) + { + //for this type of caching policy, we don't want to cache any GetAll request containing specific Ids + if (ids.Any()) return; + + //set the disposal action + SetCacheAction(() => + { + //We want to cache the result as a single collection + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); + }); + } + + /// + /// This policy will cache the full data set as a single collection + /// + /// + protected override TEntity[] GetAllFromCache() + { + var found = Cache.GetCacheItem>(GetCacheTypeKey()); + return found == null ? new TEntity[] { } : found.WhereNotNull().ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..470db33b6a --- /dev/null +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class FullDataSetRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + + public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache) + { + _runtimeCache = runtimeCache; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new FullDataSetRepositoryCachePolicy(_runtimeCache); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs new file mode 100644 index 0000000000..97844933b7 --- /dev/null +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal interface IRepositoryCachePolicy : IDisposable + where TEntity : class, IAggregateRoot + { + TEntity Get(TId id, Func getFromRepo); + TEntity Get(TId id); + bool Exists(TId id, Func getFromRepo); + + string GetCacheIdKey(object id); + string GetCacheTypeKey(); + void CreateOrUpdate(TEntity entity, Action persistMethod); + void Remove(TEntity entity, Action persistMethod); + TEntity[] GetAll(TId[] ids, Func> getFromRepo); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..2d69704b63 --- /dev/null +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs @@ -0,0 +1,9 @@ +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/OnlySingleItemsRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicy.cs new file mode 100644 index 0000000000..4aeca15863 --- /dev/null +++ b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicy.cs @@ -0,0 +1,24 @@ +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 + /// + /// + /// + internal class OnlySingleItemsRepositoryCachePolicy : DefaultRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + public OnlySingleItemsRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache, options) + { + } + + protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) + { + //do nothing + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..3559ce6ea5 --- /dev/null +++ b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs @@ -0,0 +1,27 @@ +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 OnlySingleItemsRepositoryCachePolicy(_runtimeCache, _options); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs similarity index 53% rename from src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs rename to src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index b5abedac86..e8c6ac02b0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -1,18 +1,34 @@ -namespace Umbraco.Core.Persistence.Repositories +using System; + +namespace Umbraco.Core.Cache { - internal class RepositoryCacheOptions + internal class RepositoryCachePolicyOptions { /// - /// Constructor sets defaults + /// Ctor - sets GetAllCacheValidateCount = true /// - public RepositoryCacheOptions() + public RepositoryCachePolicyOptions(Func performCount) { + PerformCount = performCount; GetAllCacheValidateCount = true; GetAllCacheAllowZeroCount = false; - GetAllCacheThresholdLimit = 100; - GetAllCacheAsCollection = false; } + /// + /// Ctor - sets GetAllCacheValidateCount = false + /// + public RepositoryCachePolicyOptions() + { + PerformCount = null; + GetAllCacheValidateCount = false; + GetAllCacheAllowZeroCount = false; + } + + /// + /// Callback required to get count for GetAllCacheValidateCount + /// + public Func PerformCount { get; private set; } + /// /// True/false as to validate the total item count when all items are returned from cache, the default is true but this /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the normal @@ -22,26 +38,11 @@ namespace Umbraco.Core.Persistence.Repositories /// setting this to return false will improve performance of GetAll cache with no params but should only be used /// for specific circumstances /// - public bool GetAllCacheValidateCount { get; set; } + public bool GetAllCacheValidateCount { get; private set; } /// /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no results found /// public bool GetAllCacheAllowZeroCount { get; set; } - - /// - /// The threshold entity count for which the GetAll method will cache entities - /// - public int GetAllCacheThresholdLimit { get; set; } - - /// - /// When set to true, the cache for the result of GetAll will be cached as a List/Collection rather than - /// individual entities in the dictionary - /// - /// - /// The default is false which means that if the result of GetAll is less than the GetAllCacheThresholdLimit, each entity - /// returned will be cached individually - /// - public bool GetAllCacheAsCollection { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index 4f588f42db..540476f93e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -28,27 +28,19 @@ namespace Umbraco.Core.Persistence.Repositories _languageRepository = languageRepository; } - /// - /// Returns the repository cache options - /// - /// - /// The dictionary repository is also backed by two sub repositories, the main one that will be used is the DictionaryByKeyRepository - /// since the queries from DefaultCultureDictionary will use this. That repositories will manage it's own caches by keys. - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { get { - return new RepositoryCacheOptions - { - //If there is zero, we can cache it - GetAllCacheAllowZeroCount = true, - GetAllCacheAsCollection = false, - GetAllCacheValidateCount = false, - //dont' cache any result with GetAll - since there could be a ton - // of dictionary items. - GetAllCacheThresholdLimit = 0 - }; + //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 + })); } } @@ -354,23 +346,19 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { get { - return new RepositoryCacheOptions - { - //If there is zero, we can cache it - GetAllCacheAllowZeroCount = true, - GetAllCacheAsCollection = false, - GetAllCacheValidateCount = false, - //dont' cache any result with GetAll - since there could be a ton - // of dictionary items. - GetAllCacheThresholdLimit = 0 - }; + //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 + })); } } } @@ -417,23 +405,19 @@ namespace Umbraco.Core.Persistence.Repositories return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { get { - return new RepositoryCacheOptions - { - //If there is zero, we can cache it - GetAllCacheAllowZeroCount = true, - GetAllCacheAsCollection = false, - GetAllCacheValidateCount = false, - //dont' cache any result with GetAll - since there could be a ton - // of dictionary items. - GetAllCacheThresholdLimit = 0 - }; + //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 + })); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 75a592c7fe..6abab73dd7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -18,26 +18,19 @@ namespace Umbraco.Core.Persistence.Repositories internal class DomainRepository : PetaPocoRepositoryBase, IDomainRepository { - private readonly RepositoryCacheOptions _cacheOptions; - public DomainRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - //Custom cache options for better performance - _cacheOptions = new RepositoryCacheOptions - { - GetAllCacheAllowZeroCount = true, - GetAllCacheValidateCount = false, - GetAllCacheAsCollection = true - }; + { } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + } } protected override IDomain PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index 53bf422d99..6fc9bd5ebc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -20,24 +21,17 @@ namespace Umbraco.Core.Persistence.Repositories { public LanguageRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - //Custom cache options for better performance - _cacheOptions = new RepositoryCacheOptions - { - GetAllCacheAllowZeroCount = true, - GetAllCacheValidateCount = false, - GetAllCacheAsCollection = true - }; + { } - private readonly RepositoryCacheOptions _cacheOptions; - - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + } } #region Overrides of RepositoryBase diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index d1127e4ac6..1086b9cee0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -17,20 +17,18 @@ namespace Umbraco.Core.Persistence.Repositories { public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - _options = new RepositoryCacheOptions - { - //We want to ensure that a zero count gets cached, even if there is nothing in the db we don't want it to lookup nothing each time - GetAllCacheAllowZeroCount = true, - //We'll use GetAll as the backing source for all queries and we'll cache the result as a single collection - GetAllCacheAsCollection = true, - //Override to false so that a Count check against the db is NOT performed when doing a GetAll without params, we just want to - // return the raw cache without validation. The GetAll on this repository gets called *A lot*, we want max performance - GetAllCacheValidateCount = false - }; + { } - private readonly RepositoryCacheOptions _options; + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + } + } protected override PublicAccessEntry PerformGet(Guid id) { @@ -94,15 +92,6 @@ namespace Umbraco.Core.Persistence.Repositories get { throw new NotImplementedException(); } } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions - { - get { return _options; } - } - - protected override void PersistNewItem(PublicAccessEntry entity) { entity.AddingEntity(); diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index c289802028..f6bcc46d06 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Persistence.Repositories { } - private readonly RepositoryCacheOptions _cacheOptions = new RepositoryCacheOptions(); + /// /// The runtime cache used for this repo by default is the isolated cache for this type @@ -92,6 +92,28 @@ 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 + { + get + { + return _cachePolicyFactory ?? (_cachePolicyFactory = new DefaultRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions(() => + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + var query = Query.Builder.Where(x => x.Id != 0); + return PerformCount(query); + }))); + } + } + /// /// Adds or Updates an entity of type TEntity /// @@ -123,23 +145,16 @@ namespace Umbraco.Core.Persistence.Repositories protected abstract TEntity PerformGet(TId id); /// - /// Gets an entity by the passed in Id utilizing the repository's runtime cache + /// Gets an entity by the passed in Id utilizing the repository's cache policy /// /// /// public TEntity Get(TId id) { - var cacheKey = GetCacheIdKey(id); - var fromCache = RuntimeCache.GetCacheItem(cacheKey); - - if (fromCache != null) return fromCache; - - var entity = PerformGet(id); - if (entity == null) return null; - - RuntimeCache.InsertCacheItem(cacheKey, () => entity); - - return entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + return p.Get(id, PerformGet); + } } protected abstract IEnumerable PerformGetAll(params TId[] ids); @@ -162,110 +177,12 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); } - if (ids.Any()) + using (var p = CachePolicyFactory.CreatePolicy()) { - var entities = ids.Select(x => RuntimeCache.GetCacheItem(GetCacheIdKey(x))).ToArray(); - - if (ids.Count().Equals(entities.Count()) && entities.Any(x => x == null) == false) - return entities; - } - else - { - TEntity[] allEntities; - if (RepositoryCacheOptions.GetAllCacheAsCollection) - { - var found = RuntimeCache.GetCacheItem>(GetCacheTypeKey()); - allEntities = found == null ? new TEntity[] {} : found.WhereNotNull().ToArray(); - } - else - { - allEntities = RuntimeCache.GetCacheItemsByKeySearch(GetCacheTypeKey()) - .WhereNotNull() - .ToArray(); - } - - if (allEntities.Any()) - { - - if (RepositoryCacheOptions.GetAllCacheValidateCount) - { - //Get count of all entities of current type (TEntity) to ensure cached result is correct - var query = Query.Builder.Where(x => x.Id != 0); - int totalCount = PerformCount(query); - - if (allEntities.Count() == totalCount) - return allEntities; - } - else - { - return allEntities; - } - } - else if (RepositoryCacheOptions.GetAllCacheAllowZeroCount) - { - //if the repository allows caching a zero count, then check the zero count cache - var zeroCount = RuntimeCache.GetCacheItem(GetCacheTypeKey()); - if (zeroCount != null && zeroCount.Any() == false) - { - //there is a zero count cache so return an empty list - return Enumerable.Empty(); - } - } - - } - - var entityCollection = PerformGetAll(ids) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull() - .ToArray(); - - //This option cannot execute if we are looking up specific Ids - if (ids.Any() == false && entityCollection.Length == 0 && RepositoryCacheOptions.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 - RuntimeCache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); - return entityCollection; - } - - //This option cannot execute if we are looking up specific Ids - if (ids.Any() == false && RepositoryCacheOptions.GetAllCacheAsCollection) - { - //when this is true, we don't want to cache each item individually, we want to cache the result as a single collection - RuntimeCache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); - return entityCollection; - } - - if (entityCollection.Length > RepositoryCacheOptions.GetAllCacheThresholdLimit) - { - //We need to put a threshold here! IF there's an insane amount of items - // coming back here we don't want to chuck it all into memory, this added cache here - // is more for convenience when paging stuff temporarily - return entityCollection; - } - - //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) - { - if (entity != null) - { - var localCopy = entity; - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy); - } - } - - return entityCollection; + return p.GetAll(ids, PerformGetAll); + } } - - /// - /// Returns the repository cache options - /// - protected virtual RepositoryCacheOptions RepositoryCacheOptions - { - get { return _cacheOptions; } - } - + protected abstract IEnumerable PerformGetByQuery(IQuery query); /// /// Gets a list of entities by the passed in query @@ -287,12 +204,10 @@ namespace Umbraco.Core.Persistence.Repositories /// public bool Exists(TId id) { - var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(id)); - if (fromCache != null) + using (var p = CachePolicyFactory.CreatePolicy()) { - return true; + return p.Exists(id, PerformExists); } - return PerformExists(id); } protected abstract int PerformCount(IQuery query); @@ -312,23 +227,12 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistNewItem(IEntity entity) { - try - { - PersistNewItem((TEntity)entity); - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - } - catch (Exception ex) - { - //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 - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - throw; - } + var casted = (TEntity)entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.CreateOrUpdate(casted, PersistNewItem); + } } /// @@ -337,23 +241,12 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistUpdatedItem(IEntity entity) { - try - { - PersistUpdatedItem((TEntity)entity); - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - } - catch (Exception) - { - //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 - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - throw; - } + var casted = (TEntity)entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.CreateOrUpdate(casted, PersistUpdatedItem); + } } /// @@ -362,10 +255,12 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistDeletedItem(IEntity entity) { - PersistDeletedItem((TEntity)entity); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); + var casted = (TEntity)entity; + + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.Remove(casted, PersistDeletedItem); + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index 195672acb4..955e316bc3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -32,7 +33,6 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITemplatesSection _templateConfig; private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; - private readonly RepositoryCacheOptions _cacheOptions; internal TemplateRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) : base(work, cache, logger, sqlSyntax) @@ -41,25 +41,18 @@ namespace Umbraco.Core.Persistence.Repositories _viewsFileSystem = viewFileSystem; _templateConfig = templateConfig; _viewHelper = new ViewHelper(_viewsFileSystem); - _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); - - _cacheOptions = new RepositoryCacheOptions - { - //Allow a zero count cache entry because GetAll() gets used quite a lot and we want to ensure - // if there are no templates, that it doesn't keep going to the db. - GetAllCacheAllowZeroCount = true, - //GetAll is used as the base call for getting all templates, so we'll cache it as a single entry - GetAllCacheAsCollection = true - }; + _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); + } } #region Overrides of RepositoryBase diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index dd83553074..baa614c3ce 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -156,12 +156,18 @@ + + + + + + @@ -169,7 +175,10 @@ + + + @@ -457,7 +466,6 @@ -