diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs index 45e79a1b67..1f51fc3ccc 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -15,35 +15,31 @@ namespace Umbraco.Core.Cache /// 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. /// - internal class DefaultRepositoryCachePolicy : DisposableObject, IRepositoryCachePolicy + internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { private readonly RepositoryCachePolicyOptions _options; - protected IRuntimeCacheProvider Cache { get; private set; } - private Action _action; public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) - { - if (cache == null) throw new ArgumentNullException("cache"); + : base(cache) + { if (options == null) throw new ArgumentNullException("options"); - - _options = options; - Cache = cache; + _options = options; } - public string GetCacheIdKey(object id) + protected string GetCacheIdKey(object id) { if (id == null) throw new ArgumentNullException("id"); return string.Format("{0}{1}", GetCacheTypeKey(), id); } - public string GetCacheTypeKey() + protected string GetCacheTypeKey() { return string.Format("uRepo_{0}_", typeof(TEntity).Name); } - public void CreateOrUpdate(TEntity entity, Action persistMethod) + public override void CreateOrUpdate(TEntity entity, Action persistMethod) { if (entity == null) throw new ArgumentNullException("entity"); if (persistMethod == null) throw new ArgumentNullException("persistMethod"); @@ -85,24 +81,29 @@ namespace Umbraco.Core.Cache } } - public void Remove(TEntity entity, Action persistMethod) + public override void Remove(TEntity entity, Action persistMethod) { if (entity == null) throw new ArgumentNullException("entity"); if (persistMethod == null) throw new ArgumentNullException("persistMethod"); - persistMethod(entity); - - //set the disposal action - var cacheKey = GetCacheIdKey(entity.Id); - SetCacheAction(() => + try { - Cache.ClearCacheItem(cacheKey); - //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.ClearCacheItem(GetCacheTypeKey()); - }); + persistMethod(entity); + } + finally + { + //set the disposal action + var cacheKey = GetCacheIdKey(entity.Id); + SetCacheAction(() => + { + Cache.ClearCacheItem(cacheKey); + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } } - public TEntity Get(TId id, Func getFromRepo) + public override TEntity Get(TId id, Func getFromRepo) { if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); @@ -119,13 +120,13 @@ namespace Umbraco.Core.Cache return entity; } - public TEntity Get(TId id) + public override TEntity Get(TId id) { var cacheKey = GetCacheIdKey(id); return Cache.GetCacheItem(cacheKey); } - public bool Exists(TId id, Func getFromRepo) + public override bool Exists(TId id, Func getFromRepo) { if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); @@ -134,7 +135,7 @@ namespace Umbraco.Core.Cache return fromCache != null || getFromRepo(id); } - public virtual TEntity[] GetAll(TId[] ids, Func> getFromRepo) + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) { if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); @@ -188,7 +189,7 @@ namespace Umbraco.Core.Cache /// Looks up the zero count cache, must return null if it doesn't exist /// /// - protected virtual bool HasZeroCountCache() + protected bool HasZeroCountCache() { var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); return (zeroCount != null && zeroCount.Any() == false); @@ -198,24 +199,13 @@ namespace Umbraco.Core.Cache /// Performs the lookup for all entities of this type from the cache /// /// - protected virtual TEntity[] GetAllFromCache() + protected 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 @@ -273,14 +263,6 @@ namespace Umbraco.Core.Cache } }); } - - /// - /// 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/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index c098af8992..a7c37ef939 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -11,24 +11,19 @@ namespace Umbraco.Core.Cache /// /// /// - /// - /// This caching policy has no sliding expiration but uses the default ObjectCache.InfiniteAbsoluteExpiration as it's timeout, so it - /// should not leave the cache unless the cache memory is exceeded and it gets thrown out. - /// - internal class FullDataSetRepositoryCachePolicy : DefaultRepositoryCachePolicy + internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IAggregateRoot { private readonly Func _getEntityId; + private readonly Func> _getAllFromRepo; + private readonly bool _expires; - public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId) : base(cache, - new RepositoryCachePolicyOptions - { - //Definitely allow zero'd cache entires since this is a full set, in many cases there will be none, - // and we must cache this! - GetAllCacheAllowZeroCount = true - }) + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId, Func> getAllFromRepo, bool expires) + : base(cache) { _getEntityId = getEntityId; + _getAllFromRepo = getAllFromRepo; + _expires = expires; } private bool? _hasZeroCountCache; @@ -47,31 +42,173 @@ namespace Umbraco.Core.Cache : result; } + + protected string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(TEntity).Name); + } + + public override void CreateOrUpdate(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + catch + { + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + throw; + } + } + + public override void Remove(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + } + finally + { + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + } + + public override TEntity Get(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return from the repo + if (found == null) return getFromRepo(id); + var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + if (entity == null) return null; + + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + return (TEntity)entity.DeepClone(); + } + + public override TEntity Get(TId id) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return null + if (found == null) return null; + var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + if (entity == null) return null; + + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + return (TEntity)entity.DeepClone(); + } + + public override bool Exists(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] {}, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return from the repo + return found == null + ? getFromRepo(id) + : found.Any(x => _getEntityId(x).Equals(id)); + } + + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + //process getting all including setting the cache callback + var result = PerformGetAll(getFromRepo); + + //now that the base result has been calculated, they will all be cached. + // Now we can just filter by ids if they have been supplied + + return (ids.Any() + ? result.Where(x => ids.Contains(_getEntityId(x))).ToArray() + : result) + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + .Select(x => (TEntity)x.DeepClone()) + .ToArray(); + } + + private TEntity[] PerformGetAll(Func> getFromRepo) + { + var allEntities = GetAllFromCache(); + if (allEntities.Any()) + { + return allEntities; + } + + //check the zero count cache + if (HasZeroCountCache()) + { + //there is a zero count cache so return an empty list + return new TEntity[] { }; + } + + //we need to do the lookup from the repo + var entityCollection = getFromRepo(new TId[] {}) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull() + .ToArray(); + + //set the disposal action + SetCacheAction(entityCollection); + + return entityCollection; + } + /// /// For this type of caching policy, we don't cache individual items /// /// /// - protected override void SetCacheAction(string cacheKey, TEntity entity) + protected void SetCacheAction(string cacheKey, TEntity entity) { - //do nothing + //No-op } /// /// Sets the action to execute on disposal for an entity collection /// - /// /// - protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) + protected void SetCacheAction(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)); + + if (_expires) + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection), + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + else + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); + } }); } @@ -79,7 +216,7 @@ namespace Umbraco.Core.Cache /// Looks up the zero count cache, must return null if it doesn't exist /// /// - protected override bool HasZeroCountCache() + protected bool HasZeroCountCache() { if (_hasZeroCountCache.HasValue) return _hasZeroCountCache.Value; @@ -92,7 +229,7 @@ namespace Umbraco.Core.Cache /// This policy will cache the full data set as a single collection /// /// - protected override TEntity[] GetAllFromCache() + protected TEntity[] GetAllFromCache() { var found = Cache.GetCacheItem>(GetCacheTypeKey()); @@ -101,5 +238,6 @@ namespace Umbraco.Core.Cache 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 index 6a79c2b8c2..e4addcf355 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache @@ -13,16 +14,20 @@ namespace Umbraco.Core.Cache { private readonly IRuntimeCacheProvider _runtimeCache; private readonly Func _getEntityId; + private readonly Func> _getAllFromRepo; + private readonly bool _expires; - public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, Func getEntityId) + 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); + 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 97844933b7..215487c3be 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -10,9 +10,7 @@ namespace Umbraco.Core.Cache 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); diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs new file mode 100644 index 0000000000..b939cd14e6 --- /dev/null +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal abstract class RepositoryCachePolicyBase : DisposableObject, IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private Action _action; + + protected RepositoryCachePolicyBase(IRuntimeCacheProvider 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(); + } + } + + /// + /// Sets the action to execute on disposal + /// + /// + protected void SetCacheAction(Action action) + { + _action = action; + } + + 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/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index 9566cd6e7f..28ac4ee2d1 100644 --- a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Cache protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) { - //do nothing + //no-op } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs index 365bf53b06..5067562aa7 100644 --- a/src/Umbraco.Core/Collections/DeepCloneableList.cs +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -14,18 +14,24 @@ namespace Umbraco.Core.Collections /// internal class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty { - /// - /// Initializes a new instance of the class that is empty and has the default initial capacity. - /// - public DeepCloneableList() + private readonly ListCloneBehavior _listCloneBehavior; + + public DeepCloneableList(ListCloneBehavior listCloneBehavior) { + _listCloneBehavior = listCloneBehavior; + } + + public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) : base(collection) + { + _listCloneBehavior = listCloneBehavior; } /// - /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. + /// Default behavior is CloneOnce /// - /// The collection whose elements are copied to the new list. is null. - public DeepCloneableList(IEnumerable collection) : base(collection) + /// + public DeepCloneableList(IEnumerable collection) + : this(collection, ListCloneBehavior.CloneOnce) { } @@ -35,20 +41,47 @@ namespace Umbraco.Core.Collections /// public object DeepClone() { - var newList = new DeepCloneableList(); - foreach (var item in this) + switch (_listCloneBehavior) { - var dc = item as IDeepCloneable; - if (dc != null) - { - newList.Add((T) dc.DeepClone()); - } - else - { - newList.Add(item); - } + case ListCloneBehavior.CloneOnce: + //we are cloning once, so create a new list in none mode + // and deep clone all items into it + var newList = new DeepCloneableList(ListCloneBehavior.None); + foreach (var item in this) + { + var dc = item as IDeepCloneable; + if (dc != null) + { + newList.Add((T)dc.DeepClone()); + } + else + { + newList.Add(item); + } + } + return newList; + case ListCloneBehavior.None: + //we are in none mode, so just return a new list with the same items + return new DeepCloneableList(this, ListCloneBehavior.None); + case ListCloneBehavior.Always: + //always clone to new list + var newList2 = new DeepCloneableList(ListCloneBehavior.Always); + foreach (var item in this) + { + var dc = item as IDeepCloneable; + if (dc != null) + { + newList2.Add((T)dc.DeepClone()); + } + else + { + newList2.Add(item); + } + } + return newList2; + default: + throw new ArgumentOutOfRangeException(); } - return newList; } public bool IsDirty() diff --git a/src/Umbraco.Core/Collections/ListCloneBehavior.cs b/src/Umbraco.Core/Collections/ListCloneBehavior.cs new file mode 100644 index 0000000000..4fe935f7ff --- /dev/null +++ b/src/Umbraco.Core/Collections/ListCloneBehavior.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Collections +{ + internal enum ListCloneBehavior + { + /// + /// When set, DeepClone will clone the items one time and the result list behavior will be None + /// + CloneOnce, + + /// + /// When set, DeepClone will not clone any items + /// + None, + + /// + /// When set, DeepClone will always clone all items + /// + Always + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index d2dea09698..4da740f458 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -136,7 +136,10 @@ namespace Umbraco.Core { try { - x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationInitialized", x.GetType()))) + { + x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -299,7 +302,10 @@ namespace Umbraco.Core { try { - x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarting", x.GetType()))) + { + x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -350,7 +356,10 @@ namespace Umbraco.Core { try { - x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarted", x.GetType()))) + { + x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 7523555c24..c1b45f63ce 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -9,10 +9,30 @@ namespace Umbraco.Core.Models { public static class DeepCloneHelper { + /// + /// Stores the metadata for the properties for a given type so we know how to create them + /// + private struct ClonePropertyInfo + { + public ClonePropertyInfo(PropertyInfo propertyInfo) : this() + { + if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); + PropertyInfo = propertyInfo; + } + + public PropertyInfo PropertyInfo { get; private set; } + public bool IsDeepCloneable { get; set; } + public Type GenericListType { get; set; } + public bool IsList + { + get { return GenericListType != null; } + } + } + /// /// Used to avoid constant reflection (perf) /// - private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); /// /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') @@ -30,81 +50,99 @@ namespace Umbraco.Core.Models throw new InvalidOperationException("Both the input and output types must be the same"); } + //get the property metadata from cache so we only have to figure this out once per type var refProperties = PropCache.GetOrAdd(inputType, type => inputType.GetProperties() - .Where(x => - //is not attributed with the ignore clone attribute - x.GetCustomAttribute() == null + .Select(propertyInfo => + { + if ( + //is not attributed with the ignore clone attribute + propertyInfo.GetCustomAttribute() != null //reference type but not string - && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) + || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof (string) //settable - && x.CanWrite + || propertyInfo.CanWrite == false //non-indexed - && x.GetIndexParameters().Any() == false) + || propertyInfo.GetIndexParameters().Any()) + { + return null; + } + + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + { + return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) + && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + { + if (propertyInfo.PropertyType.IsGenericType + && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) + { + //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); + return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; + } + if (propertyInfo.PropertyType.IsArray + || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) + { + //if its an array, we'll create a list to work with first and then convert to array later + //otherwise if its just a regular derivitave of IEnumerable, we can use a list too + return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; + } + //skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) + { + return null; + } + + //its a custom IEnumerable, we'll try to create it + try + { + var custom = Activator.CreateInstance(propertyInfo.PropertyType); + //if it's an IList we can work with it, otherwise we cannot + var newList = custom as IList; + if (newList == null) + { + return null; + } + return new ClonePropertyInfo(propertyInfo) {GenericListType = propertyInfo.PropertyType}; + } + catch (Exception) + { + //could not create this type so we'll skip it + return null; + } + } + return new ClonePropertyInfo(propertyInfo); + }) + .Where(x => x.HasValue) + .Select(x => x.Value) .ToArray()); - foreach (var propertyInfo in refProperties) + foreach (var clonePropertyInfo in refProperties) { - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + if (clonePropertyInfo.IsDeepCloneable) { //this ref property is also deep cloneable so clone it - var result = (IDeepCloneable)propertyInfo.GetValue(input, null); + var result = (IDeepCloneable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (result != null) { //set the cloned value to the property - propertyInfo.SetValue(output, result.DeepClone(), null); + clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); } } - else if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + else if (clonePropertyInfo.IsList) { - IList newList; - if (propertyInfo.PropertyType.IsGenericType - && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) - { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> - var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); - newList = (IList)Activator.CreateInstance(genericType); - } - else if (propertyInfo.PropertyType.IsArray - || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) - { - //if its an array, we'll create a list to work with first and then convert to array later - //otherwise if its just a regular derivitave of IEnumerable, we can use a list too - newList = new List(); - } - else - { - //skip instead of trying to create instance of abstract or interface - if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) - { - continue; - } - - //its a custom IEnumerable, we'll try to create it - try - { - var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //if it's an IList we can work with it, otherwise we cannot - newList = custom as IList; - if (newList == null) - { - continue; - } - } - catch (Exception) - { - //could not create this type so we'll skip it - continue; - } - } - - var enumerable = (IEnumerable)propertyInfo.GetValue(input, null); + var enumerable = (IEnumerable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (enumerable == null) continue; + var newList = (IList)Activator.CreateInstance(clonePropertyInfo.GenericListType); + var isUsableType = true; //now clone each item @@ -136,21 +174,21 @@ namespace Umbraco.Core.Models continue; } - if (propertyInfo.PropertyType.IsArray) + if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) { //need to convert to array - var arr = (object[])Activator.CreateInstance(propertyInfo.PropertyType, newList.Count); + var arr = (object[])Activator.CreateInstance(clonePropertyInfo.PropertyInfo.PropertyType, newList.Count); for (int i = 0; i < newList.Count; i++) { arr[i] = newList[i]; } //set the cloned collection - propertyInfo.SetValue(output, arr, null); + clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); } else { //set the cloned collection - propertyInfo.SetValue(output, newList, null); + clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 3cff4f0298..5f30c08ce7 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -98,10 +98,45 @@ namespace Umbraco.Core.Models.PublishedContent #endregion - + #region Cache + + // these methods are called by ContentTypeCacheRefresher and DataTypeCacheRefresher + + internal static void ClearAll() + { + Logging.LogHelper.Debug("Clear all."); + // ok and faster to do it by types, assuming noone else caches PublishedContentType instances + //ApplicationContext.Current.ApplicationCache.ClearStaticCacheByKeySearch("PublishedContentType_"); + ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes(); + } + + internal static void ClearContentType(int id) + { + Logging.LogHelper.Debug("Clear content type w/id {0}.", () => id); + // requires a predicate because the key does not contain the ID + // faster than key strings comparisons anyway + ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes( + (key, value) => value.Id == id); + } + + internal static void ClearDataType(int id) + { + Logging.LogHelper.Debug("Clear data type w/id {0}.", () => id); + // there is no recursion to handle here because a PublishedContentType contains *all* its + // properties ie both its own properties and those that were inherited (it's based upon an + // IContentTypeComposition) and so every PublishedContentType having a property based upon + // the cleared data type, be it local or inherited, will be cleared. + ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes( + (key, value) => value.PropertyTypes.Any(x => x.DataTypeId == id)); + } + public static PublishedContentType Get(PublishedItemType itemType, string alias) { - var type = CreatePublishedContentType(itemType, alias); + var key = string.Format("PublishedContentType_{0}_{1}", + itemType.ToString().ToLowerInvariant(), alias.ToLowerInvariant()); + + var type = ApplicationContext.Current.ApplicationCache.StaticCache.GetCacheItem(key, + () => CreatePublishedContentType(itemType, alias)); return type; } @@ -134,8 +169,21 @@ namespace Umbraco.Core.Models.PublishedContent return new PublishedContentType(contentType); } - // for unit tests - internal static Func GetPublishedContentTypeCallback { get; set; } - + // for unit tests - changing the callback must reset the cache obviously + private static Func _getPublishedContentTypeCallBack; + internal static Func GetPublishedContentTypeCallback + { + get { return _getPublishedContentTypeCallBack; } + set + { + // see note above + //ClearAll(); + ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheByKeySearch("PublishedContentType_"); + + _getPublishedContentTypeCallBack = value; + } + } + + #endregion } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index f4b1597a7d..22d453e150 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -230,13 +230,13 @@ namespace Umbraco.Core.Models.PublishedContent { _sourceCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.Source); _objectCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.Object); - _objectCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.XPath); + _xpathCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.XPath); } else { _sourceCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Source); _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Object); - _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); + _xpathCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); } if (_objectCacheLevel < _sourceCacheLevel) _objectCacheLevel = _sourceCacheLevel; if (_xpathCacheLevel < _sourceCacheLevel) _xpathCacheLevel = _sourceCacheLevel; diff --git a/src/Umbraco.Core/Persistence/PetaPoco.cs b/src/Umbraco.Core/Persistence/PetaPoco.cs index d55ad8fc14..88f90639d1 100644 --- a/src/Umbraco.Core/Persistence/PetaPoco.cs +++ b/src/Umbraco.Core/Persistence/PetaPoco.cs @@ -834,7 +834,7 @@ namespace Umbraco.Core.Persistence var pd = PocoData.ForType(typeof(T)); try { - r = cmd.ExecuteReader(); + r = cmd.ExecuteReaderWithRetry(); OnExecutedCommand(cmd); } catch (Exception x) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index e7e98b28ac..fc2e49a8e5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -646,13 +646,10 @@ AND umbracoNode.id <> @id", var allParentIdsAsArray = allParentContentTypeIds.SelectMany(x => x.Value).Distinct().ToArray(); if (allParentIdsAsArray.Any()) { - //NO!!!!!!!!!!! Do not recurse lookup, we've already looked them all up - //var allParentContentTypes = contentTypeRepository.GetAll(allParentIdsAsArray).ToArray(); - var allParentContentTypes = contentTypes.Where(x => allParentIdsAsArray.Contains(x.Id)).ToArray(); foreach (var contentType in contentTypes) - { + { var entityId = contentType.Id; var parentContentTypes = allParentContentTypes.Where(x => @@ -717,10 +714,10 @@ AND umbracoNode.id <> @id", out IDictionary> parentMediaTypeIds) { Mandate.ParameterNotNull(db, "db"); - + var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId, ParentTypes.parentContentTypeKey as chtParentKey, umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, @@ -744,7 +741,7 @@ AND umbracoNode.id <> @id", ON ParentTypes.childContentTypeId = cmsContentType.nodeId WHERE (umbracoNode.nodeObjectType = @nodeObjectType) ORDER BY ctId"; - + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); if (result.Any() == false) @@ -761,6 +758,7 @@ AND umbracoNode.id <> @id", // we used to do. var queue = new Queue(result); var currAllowedContentTypes = new List(); + while (queue.Count > 0) { var ct = queue.Dequeue(); @@ -841,8 +839,8 @@ AND umbracoNode.id <> @id", //now create the content type object - var factory = new ContentTypeFactory(); - var mediaType = factory.BuildMediaTypeEntity(contentTypeDto); + var factory = new MediaTypeFactory(new Guid(Constants.ObjectTypes.MediaType)); + var mediaType = factory.BuildEntity(contentTypeDto); //map the allowed content types mediaType.AllowedContentTypes = currAllowedContentTypes; @@ -850,16 +848,16 @@ AND umbracoNode.id <> @id", return mediaType; } - internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, + internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, out IDictionary> associatedTemplates, out IDictionary> parentContentTypeIds) { Mandate.ParameterNotNull(db, "db"); - + var sql = @"SELECT cmsDocumentType.IsDefault as dtIsDefault, cmsDocumentType.templateNodeId as dtTemplateId, cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId,ParentTypes.parentContentTypeKey as chtParentKey, umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, @@ -892,7 +890,7 @@ AND umbracoNode.id <> @id", ON ParentTypes.childContentTypeId = cmsContentType.nodeId WHERE (umbracoNode.nodeObjectType = @nodeObjectType) ORDER BY ctId"; - + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType)}); if (result.Any() == false) @@ -913,7 +911,7 @@ AND umbracoNode.id <> @id", { var ct = queue.Dequeue(); - //check for default templates + //check for default templates bool? isDefaultTemplate = Convert.ToBoolean(ct.dtIsDefault); int? templateId = ct.dtTemplateId; if (currDefaultTemplate == -1 && isDefaultTemplate.HasValue && templateId.HasValue) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 1c67aa0cf5..b7b4ddd583 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -37,7 +37,10 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires:true)); } } @@ -63,13 +66,17 @@ namespace Umbraco.Core.Persistence.Repositories { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.Text, SqlSyntax); + var sql = translator.Translate(); var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) - : Enumerable.Empty(); + + return + //This returns a lookup from the GetAll cached looup + (dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) + : Enumerable.Empty()) + //order the result by name + .OrderBy(x => x.Name); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 563243f12c..7b6cc162a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -29,7 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index 3884eac888..f9a8e59cfa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -30,7 +30,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 86c0927664..50a89bfd65 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -34,7 +34,10 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); } } @@ -60,13 +63,17 @@ namespace Umbraco.Core.Persistence.Repositories { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.Text, SqlSyntax); + var sql = translator.Translate(); var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) - : Enumerable.Empty(); + + return + //This returns a lookup from the GetAll cached looup + (dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) + : Enumerable.Empty()) + //order the result by name + .OrderBy(x => x.Name); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 581ca3c4a6..b35c793400 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -33,7 +33,10 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 1d8e56190b..22fad9d99b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -26,7 +26,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index 3545bc1c55..fa780e1bd0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -51,7 +51,8 @@ namespace Umbraco.Core.Persistence.Repositories get { //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection - return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); } } @@ -488,7 +489,7 @@ namespace Umbraco.Core.Persistence.Repositories var parent = all.FirstOrDefault(x => x.Id == masterTemplateId); if (parent == null) return Enumerable.Empty(); - var children = all.Where(x => x.MasterTemplateAlias == parent.Alias); + var children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); return children; } @@ -497,7 +498,7 @@ namespace Umbraco.Core.Persistence.Repositories //return from base.GetAll, this is all cached return base.GetAll().Where(x => alias.IsNullOrWhiteSpace() ? x.MasterTemplateAlias.IsNullOrWhiteSpace() - : x.MasterTemplateAlias == alias); + : x.MasterTemplateAlias.InvariantEquals(alias)); } public IEnumerable GetDescendants(int masterTemplateId) @@ -532,7 +533,7 @@ namespace Umbraco.Core.Persistence.Repositories var descendants = new List(); if (alias.IsNullOrWhiteSpace() == false) { - var parent = all.FirstOrDefault(x => x.Alias == alias); + var parent = all.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); if (parent == null) return Enumerable.Empty(); //recursively add all children AddChildren(all, descendants, parent.Alias); @@ -552,7 +553,7 @@ namespace Umbraco.Core.Persistence.Repositories private void AddChildren(ITemplate[] all, List descendants, string masterAlias) { - var c = all.Where(x => x.MasterTemplateAlias == masterAlias).ToArray(); + var c = all.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); descendants.AddRange(c); if (c.Any() == false) return; //recurse through all children @@ -573,7 +574,7 @@ namespace Umbraco.Core.Persistence.Repositories //first get all template objects var allTemplates = base.GetAll().ToArray(); - var selfTemplate = allTemplates.SingleOrDefault(x => x.Alias == alias); + var selfTemplate = allTemplates.SingleOrDefault(x => x.Alias.InvariantEquals(alias)); if (selfTemplate == null) { return null; @@ -582,11 +583,11 @@ namespace Umbraco.Core.Persistence.Repositories var top = selfTemplate; while (top.MasterTemplateAlias.IsNullOrWhiteSpace() == false) { - top = allTemplates.Single(x => x.Alias == top.MasterTemplateAlias); + top = allTemplates.Single(x => x.Alias.InvariantEquals(top.MasterTemplateAlias)); } var topNode = new TemplateNode(allTemplates.Single(x => x.Id == top.Id)); - var childTemplates = allTemplates.Where(x => x.MasterTemplateAlias == top.Alias); + var childTemplates = allTemplates.Where(x => x.MasterTemplateAlias.InvariantEquals(top.Alias)); //This now creates the hierarchy recursively topNode.Children = CreateChildren(topNode, childTemplates, allTemplates); @@ -598,7 +599,7 @@ namespace Umbraco.Core.Persistence.Repositories private static TemplateNode WalkTree(TemplateNode current, string alias) { //now walk the tree to find the node - if (current.Template.Alias == alias) + if (current.Template.Alias.InvariantEquals(alias)) { return current; } @@ -730,7 +731,7 @@ namespace Umbraco.Core.Persistence.Repositories //get this node's children var local = childTemplate; - var kids = allTemplates.Where(x => x.MasterTemplateAlias == local.Alias); + var kids = allTemplates.Where(x => x.MasterTemplateAlias.InvariantEquals(local.Alias)); //recurse child.Children = CreateChildren(child, kids, allTemplates); @@ -760,7 +761,7 @@ namespace Umbraco.Core.Persistence.Repositories private bool AliasAlreadExists(ITemplate template) { - var sql = GetBaseQuery(true).Where(x => x.Alias == template.Alias && x.NodeId != template.Id); + var sql = GetBaseQuery(true).Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); var count = Database.ExecuteScalar(sql); return count > 0; } diff --git a/src/Umbraco.Core/Profiling/StartupWebProfilerProvider.cs b/src/Umbraco.Core/Profiling/StartupWebProfilerProvider.cs deleted file mode 100644 index 16ce638c0e..0000000000 --- a/src/Umbraco.Core/Profiling/StartupWebProfilerProvider.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Threading; -using System.Web; -using StackExchange.Profiling; - -namespace Umbraco.Core.Profiling -{ - /// - /// Allows us to profile items during app startup - before an HttpRequest is created - /// - internal class StartupWebProfilerProvider : WebRequestProfilerProvider - { - public StartupWebProfilerProvider() - { - _startupPhase = StartupPhase.Boot; - //create the startup profiler - _startupProfiler = new MiniProfiler("http://localhost/umbraco-startup", ProfileLevel.Verbose) - { - Name = "StartupProfiler" - }; - } - - private MiniProfiler _startupProfiler; - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - - private enum StartupPhase - { - None = 0, - Boot = 1, - Request = 2 - } - - private volatile StartupPhase _startupPhase; - - public void BootComplete() - { - using (new ReadLock(_locker)) - { - if (_startupPhase != StartupPhase.Boot) return; - } - - using (var l = new UpgradeableReadLock(_locker)) - { - if (_startupPhase == StartupPhase.Boot) - { - l.UpgradeToWriteLock(); - - ////Now we need to transfer some information from our startup phase to the normal - ////web request phase to output the startup profiled information. - ////Stop our internal startup profiler, this will write out it's results to storage. - //StopProfiler(_startupProfiler); - //SaveProfiler(_startupProfiler); - - _startupPhase = StartupPhase.Request; - } - } - } - - public override void Stop(bool discardResults) - { - using (new ReadLock(_locker)) - { - if (_startupPhase == StartupPhase.None) - { - base.Stop(discardResults); - return; - } - } - - using (var l = new UpgradeableReadLock(_locker)) - { - if (_startupPhase > 0 && base.GetCurrentProfiler() == null) - { - l.UpgradeToWriteLock(); - - _startupPhase = StartupPhase.None; - - if (HttpContext.Current != null) - { - HttpContext.Current.Items[":mini-profiler:"] = _startupProfiler; - base.Stop(discardResults); - _startupProfiler = null; - } - } - else - { - base.Stop(discardResults); - } - } - } - - public override MiniProfiler Start(ProfileLevel level) - { - using (new ReadLock(_locker)) - { - if (_startupPhase > 0 && base.GetCurrentProfiler() == null) - { - SetProfilerActive(_startupProfiler); - return _startupProfiler; - } - - return base.Start(level); - } - } - - public override MiniProfiler GetCurrentProfiler() - { - using (new ReadLock(_locker)) - { - if (_startupPhase > 0) - { - try - { - var current = base.GetCurrentProfiler(); - if (current == null) return _startupProfiler; - } - catch - { - return _startupProfiler; - } - } - - return base.GetCurrentProfiler(); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Profiling/WebProfiler.cs b/src/Umbraco.Core/Profiling/WebProfiler.cs index 7e2cf49313..00d088bca7 100644 --- a/src/Umbraco.Core/Profiling/WebProfiler.cs +++ b/src/Umbraco.Core/Profiling/WebProfiler.cs @@ -12,24 +12,15 @@ namespace Umbraco.Core.Profiling /// internal class WebProfiler : IProfiler { - private StartupWebProfilerProvider _startupWebProfilerProvider; /// /// Constructor - /// + /// + /// + /// Binds to application events to enable the MiniProfiler + /// internal WebProfiler() { - //setup some defaults - MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); - MiniProfiler.Settings.StackMaxLength = 5000; - - //At this point we know that we've been constructed during app startup, there won't be an HttpRequest in the HttpContext - // since it hasn't started yet. So we need to do some hacking to enable profiling during startup. - _startupWebProfilerProvider = new StartupWebProfilerProvider(); - //this should always be the case during startup, we'll need to set a custom profiler provider - MiniProfiler.Settings.ProfilerProvider = _startupWebProfilerProvider; - - //Binds to application events to enable the MiniProfiler with a real HttpRequest UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; } @@ -62,12 +53,7 @@ namespace Umbraco.Core.Profiling /// void UmbracoApplicationEndRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) - { - Stop(); - _startupWebProfilerProvider = null; - } - else if (CanPerformProfilingAction(sender)) + if (CanPerformProfilingAction(sender)) { Stop(); } @@ -80,11 +66,6 @@ namespace Umbraco.Core.Profiling /// void UmbracoApplicationBeginRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) - { - _startupWebProfilerProvider.BootComplete(); - } - if (CanPerformProfilingAction(sender)) { Start(); @@ -143,7 +124,9 @@ namespace Umbraco.Core.Profiling /// Start the profiler /// public void Start() - { + { + MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); + MiniProfiler.Settings.StackMaxLength = 5000; MiniProfiler.Start(); } diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 8529132bb5..9c46ae69f4 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -9,6 +9,12 @@ namespace Umbraco.Core.Security { public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory { + public BackOfficeClaimsIdentityFactory() + { + SecurityStampClaimType = Constants.Security.SessionIdClaimType; + UserNameClaimType = ClaimTypes.Name; + } + /// /// Create a ClaimsIdentity from a user /// @@ -20,7 +26,7 @@ namespace Umbraco.Core.Security var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, //set a new session id - new UserData(Guid.NewGuid().ToString("N")) + new UserData { Id = user.Id, Username = user.UserName, @@ -29,7 +35,8 @@ namespace Umbraco.Core.Security Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId + StartMediaNode = user.StartMediaId, + SessionId = user.SecurityStamp }); return umbracoIdentity; diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 6ce81f3e9f..1bc9902da5 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -209,17 +209,19 @@ namespace Umbraco.Core.Security if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this)); - - ////TODO: Not sure why this is null sometimes, it shouldn't be. Somewhere it's not being set - /// I think it's due to some bug I had in chrome, we'll see - //if (UserData.SessionId.IsNullOrWhiteSpace()) - //{ - // UserData.SessionId = Guid.NewGuid().ToString(); - //} - if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false) + if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false) + { AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + //The security stamp claim is also required... this is because this claim type is hard coded + // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444 + if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false) + { + AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + //Add each app as a separate claim if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false) { diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs index ff49636217..407d2782dd 100644 --- a/src/Umbraco.Core/Security/UserData.cs +++ b/src/Umbraco.Core/Security/UserData.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Security /// Use this constructor to create/assign new UserData to the ticket /// /// - /// A unique id that is assigned to this ticket + /// The security stamp for the user /// public UserData(string sessionId) { @@ -30,8 +30,7 @@ namespace Umbraco.Core.Security } /// - /// This is used to Id the current ticket which we can then use to mitigate csrf attacks - /// and other things that require request validation. + /// This is the 'security stamp' for validation /// [DataMember(Name = "sessionId")] public string SessionId { get; set; } @@ -42,8 +41,6 @@ namespace Umbraco.Core.Security [DataMember(Name = "roles")] public string[] Roles { get; set; } - //public int SessionTimeout { get; set; } - [DataMember(Name = "username")] public string Username { get; set; } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 439b18d9c6..e7298a5c6e 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1227,6 +1227,13 @@ namespace Umbraco.Core.Services /// Optional Id of the user issueing the delete operation public void DeleteContentOfType(int contentTypeId, int userId = 0) { + //TODO: This currently this is called from the ContentTypeService but that needs to change, + // if we are deleting a content type, we should just delete the data and do this operation slightly differently. + // This method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + using (new WriteLock(Locker)) { using (var uow = UowProvider.GetUnitOfWork()) diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 4b29dd039e..5f337dc696 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -791,6 +791,13 @@ namespace Umbraco.Core.Services using (new WriteLock(Locker)) { + + //TODO: This needs to change, if we are deleting a content type, we should just delete the data, + // this method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + _contentService.DeleteContentOfType(contentType.Id); var uow = UowProvider.GetUnitOfWork(); diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index a27b114f4d..87fc694fd1 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -60,7 +60,7 @@ namespace Umbraco.Core.Sync protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType) { - // we don't care if there's servers listed or not, + // we don't care if there's servers listed or not, // if distributed call is enabled we will make the call return _initialized && DistributedEnabled; } @@ -139,12 +139,35 @@ namespace Umbraco.Core.Sync { if (_released) return; + var coldboot = false; if (_lastId < 0) // never synced before { - // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. - _logger.Warn("No last synced Id found, this generally means this is a new server/install. The server will rebuild its caches and indexes and then adjust it's last synced id to the latest found in the database and will start maintaining cache updates based on that id"); + _logger.Warn("No last synced Id found, this generally means this is a new server/install." + + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" + + " the database and maintain cache updates based on that Id."); + coldboot = true; + } + else + { + //check for how many instructions there are to process + var count = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); + if (count > _options.MaxProcessingInstructionCount) + { + //too many instructions, proceed to cold boot + _logger.Warn("The instruction count ({0}) exceeds the specified MaxProcessingInstructionCount ({1})." + + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + + " to the latest found in the database and maintain cache updates based on that Id.", + () => count, () => _options.MaxProcessingInstructionCount); + + coldboot = true; + } + } + + if (coldboot) + { // go get the last id in the db and store it // note: do it BEFORE initializing otherwise some instructions might get lost // when doing it before, some instructions might run twice - not an issue @@ -169,7 +192,7 @@ namespace Umbraco.Core.Sync { lock (_locko) { - if (_syncing) + if (_syncing) return; if (_released) @@ -213,9 +236,9 @@ namespace Umbraco.Core.Sync private void ProcessDatabaseInstructions() { // NOTE - // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that + // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that // would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests - // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are + // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are // pending requests after being processed, they'll just be processed on the next poll. // // FIXME not true if we're running on a background thread, assuming we can? @@ -281,7 +304,7 @@ namespace Umbraco.Core.Sync /// Remove old instructions from the database /// /// - /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause + /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause /// the site to cold boot if there's been no instruction activity for more than DaysToRetainInstructions. /// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085 /// @@ -290,15 +313,15 @@ namespace Umbraco.Core.Sync var pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions); var sqlSyntax = _appContext.DatabaseContext.SqlSyntax; - //NOTE: this query could work on SQL server and MySQL: + //NOTE: this query could work on SQL server and MySQL: /* SELECT id FROM umbracoCacheInstruction - WHERE utcStamp < getdate() + WHERE utcStamp < getdate() AND id <> (SELECT MAX(id) FROM umbracoCacheInstruction) */ // However, this will not work on SQLCE and in fact it will be slower than the query we are - // using if the SQL server doesn't perform it's own query optimizations (i.e. since the above + // using if the SQL server doesn't perform it's own query optimizations (i.e. since the above // query could actually execute a sub query for every row found). So we've had to go with an // inner join which is faster and works on SQLCE but it's uglier to read. @@ -331,9 +354,9 @@ namespace Umbraco.Core.Sync var dtos = _appContext.DatabaseContext.Database.Fetch(sql); if (dtos.Count == 0) - _lastId = -1; + _lastId = -1; } - + /// /// Reads the last-synced id from file into memory. /// @@ -502,4 +525,3 @@ namespace Umbraco.Core.Sync #endregion } } - \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs index f1bebce10b..7559c37813 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; namespace Umbraco.Core.Sync -{ +{ /// /// Provides options to the . /// @@ -14,9 +14,15 @@ namespace Umbraco.Core.Sync public DatabaseServerMessengerOptions() { DaysToRetainInstructions = 2; // 2 days - ThrottleSeconds = 5; // 5 seconds + ThrottleSeconds = 5; // 5 second + MaxProcessingInstructionCount = 1000; } + /// + /// The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches). + /// + public int MaxProcessingInstructionCount { get; set; } + /// /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index cee2f8ee3f..34c045ce7e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -175,6 +175,7 @@ + @@ -188,6 +189,7 @@ + @@ -484,7 +486,6 @@ - diff --git a/src/Umbraco.Tests/Cache/DeepCloneRuntimeCacheProviderTests.cs b/src/Umbraco.Tests/Cache/DeepCloneRuntimeCacheProviderTests.cs index 63225f6725..39e5dd2cb1 100644 --- a/src/Umbraco.Tests/Cache/DeepCloneRuntimeCacheProviderTests.cs +++ b/src/Umbraco.Tests/Cache/DeepCloneRuntimeCacheProviderTests.cs @@ -41,7 +41,7 @@ namespace Umbraco.Tests.Cache [Test] public void Clones_List() { - var original = new DeepCloneableList(); + var original = new DeepCloneableList(ListCloneBehavior.Always); original.Add(new DeepCloneableListTests.TestClone()); original.Add(new DeepCloneableListTests.TestClone()); original.Add(new DeepCloneableListTests.TestClone()); diff --git a/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs b/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs index 32381b593b..9b0aaac78b 100644 --- a/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/DefaultCachePolicyTests.cs @@ -120,5 +120,37 @@ namespace Umbraco.Tests.Cache Assert.IsTrue(cacheCleared); } } + + [Test] + public void If_Removes_Throws_Cache_Is_Removed() + { + var cacheCleared = false; + var cache = new Mock(); + cache.Setup(x => x.ClearCacheItem(It.IsAny())) + .Callback(() => + { + cacheCleared = true; + }); + + 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!"); + }); + } + } + catch + { + //we need this catch or nunit throw up + } + finally + { + Assert.IsTrue(cacheCleared); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs index 9187fe5b27..96e22e3aff 100644 --- a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs @@ -14,9 +14,57 @@ namespace Umbraco.Tests.Cache [TestFixture] public class FullDataSetCachePolicyTests { + [Test] + public void Caches_Single() + { + var getAll = new[] + { + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }; + + var isCached = false; + var cache = new Mock(); + cache.Setup(x => x.InsertCacheItem(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => + { + 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)); + } + Assert.IsTrue(isCached); + } + + [Test] + public void Get_Single_From_Cache() + { + var getAll = new[] + { + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }; + + 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); + } + } + [Test] public void Get_All_Caches_Empty_List() { + var getAll = new AuditItem[] {}; + var cached = new List(); IList list = null; @@ -33,23 +81,23 @@ namespace Umbraco.Tests.Cache cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(() => { //return null if this is the first pass - return cached.Any() ? new DeepCloneableList() : null; + return cached.Any() ? new DeepCloneableList(ListCloneBehavior.CloneOnce) : null; }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); using (defaultPolicy) { - var found = defaultPolicy.GetAll(new object[] {}, o => new AuditItem[] {}); + var found = defaultPolicy.GetAll(new object[] {}, o => 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); + defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); using (defaultPolicy) { - var found = defaultPolicy.GetAll(new object[] { }, o => new AuditItem[] { }); + var found = defaultPolicy.GetAll(new object[] { }, o => getAll); } Assert.AreEqual(1, cached.Count); @@ -59,6 +107,12 @@ namespace Umbraco.Tests.Cache [Test] public void Get_All_Caches_As_Single_List() { + var getAll = new[] + { + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }; + var cached = new List(); IList list = null; @@ -73,14 +127,10 @@ namespace Umbraco.Tests.Cache }); cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem[] { }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); using (defaultPolicy) { - var found = defaultPolicy.GetAll(new object[] { }, o => new[] - { - new AuditItem(1, "blah", AuditType.Copy, 123), - new AuditItem(2, "blah2", AuditType.Copy, 123) - }); + var found = defaultPolicy.GetAll(new object[] { }, o => getAll); } Assert.AreEqual(1, cached.Count); @@ -89,21 +139,99 @@ namespace Umbraco.Tests.Cache [Test] public void Get_All_Without_Ids_From_Cache() - { + { + var getAll = new[] { (AuditItem)null }; + var cache = new Mock(); - cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(() => new DeepCloneableList + cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(() => new DeepCloneableList(ListCloneBehavior.CloneOnce) { new AuditItem(1, "blah", AuditType.Copy, 123), new AuditItem(2, "blah2", AuditType.Copy, 123) }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); using (defaultPolicy) { - var found = defaultPolicy.GetAll(new object[] { }, o => new[] { (AuditItem)null }); + var found = defaultPolicy.GetAll(new object[] { }, o => getAll); Assert.AreEqual(2, found.Length); } } + + [Test] + public void If_CreateOrUpdate_Throws_Cache_Is_Removed() + { + var getAll = new[] + { + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }; + + var cacheCleared = false; + var cache = new Mock(); + cache.Setup(x => x.ClearCacheItem(It.IsAny())) + .Callback(() => + { + cacheCleared = true; + }); + + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); + try + { + using (defaultPolicy) + { + defaultPolicy.CreateOrUpdate(new AuditItem(1, "blah", AuditType.Copy, 123), item => + { + throw new Exception("blah!"); + }); + } + } + catch + { + //we need this catch or nunit throw up + } + finally + { + Assert.IsTrue(cacheCleared); + } + } + + [Test] + public void If_Removes_Throws_Cache_Is_Removed() + { + var getAll = new[] + { + new AuditItem(1, "blah", AuditType.Copy, 123), + new AuditItem(2, "blah2", AuditType.Copy, 123) + }; + + var cacheCleared = false; + var cache = new Mock(); + cache.Setup(x => x.ClearCacheItem(It.IsAny())) + .Callback(() => + { + cacheCleared = true; + }); + + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id, () => getAll, false); + try + { + using (defaultPolicy) + { + defaultPolicy.Remove(new AuditItem(1, "blah", AuditType.Copy, 123), item => + { + throw new Exception("blah!"); + }); + } + } + catch + { + //we need this catch or nunit throw up + } + finally + { + Assert.IsTrue(cacheCleared); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs b/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs index fcc50df60c..d478192e02 100644 --- a/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs +++ b/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs @@ -12,10 +12,44 @@ namespace Umbraco.Tests.Collections [TestFixture] public class DeepCloneableListTests { + [Test] + public void Deep_Clones_Each_Item_Once() + { + var list = new DeepCloneableList(ListCloneBehavior.CloneOnce); + list.Add(new TestClone()); + list.Add(new TestClone()); + list.Add(new TestClone()); + + var cloned = list.DeepClone() as DeepCloneableList; + + //Test that each item in the sequence is equal - based on the equality comparer of TestClone (i.e. it's ID) + Assert.IsTrue(list.SequenceEqual(cloned)); + + //Test that each instance in the list is not the same one + foreach (var item in list) + { + var clone = cloned.Single(x => x.Id == item.Id); + Assert.AreNotSame(item, clone); + } + + //clone again from the clone - since it's clone once the items should be the same + var cloned2 = cloned.DeepClone() as DeepCloneableList; + + //Test that each item in the sequence is equal - based on the equality comparer of TestClone (i.e. it's ID) + Assert.IsTrue(cloned.SequenceEqual(cloned2)); + + //Test that each instance in the list is the same one + foreach (var item in cloned) + { + var clone = cloned2.Single(x => x.Id == item.Id); + Assert.AreSame(item, clone); + } + } + [Test] public void Deep_Clones_All_Elements() { - var list = new DeepCloneableList(); + var list = new DeepCloneableList(ListCloneBehavior.Always); list.Add(new TestClone()); list.Add(new TestClone()); list.Add(new TestClone()); @@ -30,7 +64,7 @@ namespace Umbraco.Tests.Collections [Test] public void Clones_Each_Item() { - var list = new DeepCloneableList(); + var list = new DeepCloneableList(ListCloneBehavior.Always); list.Add(new TestClone()); list.Add(new TestClone()); list.Add(new TestClone()); @@ -46,7 +80,7 @@ namespace Umbraco.Tests.Collections [Test] public void Cloned_Sequence_Equals() { - var list = new DeepCloneableList(); + var list = new DeepCloneableList(ListCloneBehavior.Always); list.Add(new TestClone()); list.Add(new TestClone()); list.Add(new TestClone()); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 05295bf3a1..053f71b7e0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -12,7 +12,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; - +using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Tests.TestHelpers; @@ -514,6 +514,35 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Query_On_ContentTypeRepository_Sort_By_Name() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var contentType = repository.Get(NodeDto.NodeIdSeed + 1); + var child1 = MockedContentTypes.CreateSimpleContentType("aabc", "aabc", contentType, randomizeAliases: true); + repository.AddOrUpdate(child1); + var child3 = MockedContentTypes.CreateSimpleContentType("zyx", "zyx", contentType, randomizeAliases: true); + repository.AddOrUpdate(child3); + var child2 = MockedContentTypes.CreateSimpleContentType("a123", "a123", contentType, randomizeAliases: true); + repository.AddOrUpdate(child2); + unitOfWork.Commit(); + + // Act + var contentTypes = repository.GetByQuery(new Query().Where(x => x.ParentId == contentType.Id)); + + // Assert + Assert.That(contentTypes.Count(), Is.EqualTo(3)); + Assert.AreEqual("a123", contentTypes.ElementAt(0).Name); + Assert.AreEqual("aabc", contentTypes.ElementAt(1).Name); + Assert.AreEqual("zyx", contentTypes.ElementAt(2).Name); + } + + } + [Test] public void Can_Perform_Get_On_ContentTypeRepository() { diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs index 4119ab4f86..2e3bc39814 100644 --- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs @@ -105,7 +105,7 @@ namespace Umbraco.Tests.Security var identity = new UmbracoBackOfficeIdentity(userData); - Assert.AreEqual(10, identity.Claims.Count()); + Assert.AreEqual(11, identity.Claims.Count()); } [Test] @@ -132,7 +132,7 @@ namespace Umbraco.Tests.Security var backofficeIdentity = new UmbracoBackOfficeIdentity(claimsIdentity, userData); - Assert.AreEqual(12, backofficeIdentity.Claims.Count()); + Assert.AreEqual(13, backofficeIdentity.Claims.Count()); } [Test] @@ -156,7 +156,7 @@ namespace Umbraco.Tests.Security var identity = new UmbracoBackOfficeIdentity(ticket); - Assert.AreEqual(11, identity.Claims.Count()); + Assert.AreEqual(12, identity.Claims.Count()); } [Test] @@ -182,7 +182,7 @@ namespace Umbraco.Tests.Security var cloned = identity.Clone(); - Assert.AreEqual(11, cloned.Claims.Count()); + Assert.AreEqual(12, cloned.Claims.Count()); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 7e5645d6cd..0d41eed050 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -78,3 +78,94 @@ iframe, .content-column-body { .icon-chevron-down:before { content: "\e0c9"; } + + +/* Styling for validation in Public Access */ + +.pa-umb-overlay { + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.pa-umb-overlay + .pa-umb-overlay { + padding-top: 30px; + border-top: 1px solid @grayLight; +} + +.pa-select-type { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + justify-content: center; + align-items: flex-start; + + margin-top: 15px; +} + +.pa-select-type label { + padding: 0 20px; +} + +.pa-access-header { + font-weight: bold; + margin: 0 0 3px 0; + padding-bottom: 0; +} + +.pa-access-description { + color: #b3b3b3; + margin: 0; +} + +.pa-validation-message { + padding: 6px 12px !important; + margin: 5px 0 0 0 !important; + display: inline-block; +} + +.pa-select-pages label { + margin: 0; + font-size: 15px; +} + +.pa-select-pages label + .controls-row { + padding-top: 0; +} + +.pa-select-pages .umb-detail { + font-size: 13px; + margin: 2px 0 5px; +} + +.pa-choose-page a { + color: @blue; + font-size: 15px; +} + +.pa-choose-page a:hover, .pa-choose-page a:active, .pa-choose-page a:focus { + color: @blueDark; + text-decoration: none; +} + +.pa-choose-page a:before { + content:"+"; + margin-right: 3px; + font-weight: bold; +} + +.pa-choose-page .treePickerTitle { + font-weight: bold; + font-size: 13px; + font-style: italic; + background: whitesmoke; + padding: 3px 5px; + color: grey; + + border-bottom: none; +} + + +.pa-form + .pa-form { + margin-top: 10px; +} diff --git a/src/Umbraco.Web.UI/config/ExamineSettings.config b/src/Umbraco.Web.UI/config/ExamineSettings.config index 6759b5a21d..4e82ca2bb8 100644 --- a/src/Umbraco.Web.UI/config/ExamineSettings.config +++ b/src/Umbraco.Web.UI/config/ExamineSettings.config @@ -6,12 +6,12 @@ Index sets can be defined in the ExamineIndex.config if you're using the standar More information and documentation can be found on CodePlex: http://umbracoexamine.codeplex.com --> - + @@ -22,7 +22,7 @@ More information and documentation can be found on CodePlex: http://umbracoexami useTempStorage="Sync"/> - @@ -31,15 +31,15 @@ More information and documentation can be found on CodePlex: http://umbracoexami - diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx index 0a424f506f..be515f693c 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/protectPage.aspx @@ -5,30 +5,30 @@