From 6c5e09fd84efb7989d21d42038c81650d6315b0a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Jan 2016 19:26:08 +0100 Subject: [PATCH 01/31] Ensures that the main content type repositories are returning the correct result from their GetAll methods. Fixes the FullDataSetRepositoryCachePolicy to ensure that any request with Ids actually returns the result of GetAll and then filters to ensure caching is correct. --- .../Cache/FullDataSetRepositoryCachePolicy.cs | 14 ++++++ .../Repositories/ContentTypeRepository.cs | 15 +++---- .../Repositories/MediaTypeRepository.cs | 22 +++------- .../Repositories/MemberTypeRepository.cs | 44 ++++--------------- .../Repositories/RepositoryBase.cs | 3 +- 5 files changed, 37 insertions(+), 61 deletions(-) diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index 3b3c98fc80..b435615d54 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -30,6 +30,20 @@ namespace Umbraco.Core.Cache private bool? _hasZeroCountCache; + + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + //process the base logic without any Ids - we want to cache them all! + var result = base.GetAll(new TId[] { }, 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((TId) (object) x.Id)).ToArray() + : result; + } + /// /// For this type of caching policy, we don't cache individual items /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 8ab98e86e4..2fdc0e8613 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -48,14 +48,12 @@ namespace Umbraco.Core.Persistence.Repositories { if (ids.Any()) { - return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); - } - else - { - var sql = new Sql().Select("id").From().Where(dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); + //NOTE: This logic should never be executed according to our cache policy + return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository) + .Where(x => ids.Contains(x.Id)); } + + return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -269,9 +267,6 @@ namespace Umbraco.Core.Persistence.Repositories else { return GetAll(); - //var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - //var allIds = Database.Fetch(sql).ToArray(); - //return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 68f7a3c0d9..9147cead55 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -37,25 +37,20 @@ namespace Umbraco.Core.Persistence.Repositories protected override IMediaType PerformGet(int id) { - var contentTypes = ContentTypeQueryMapper.GetMediaTypes( - new[] { id }, Database, SqlSyntax, this); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return ContentTypeQueryMapper.GetMediaTypes(ids, Database, SqlSyntax, this); - } - else - { - var sql = new Sql().Select("id").From().Where(dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetMediaTypes(allIds, Database, SqlSyntax, this); + //NOTE: This logic should never be executed according to our cache policy + return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this) + .Where(x => ids.Contains(x.Id)); } + + return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -190,9 +185,6 @@ namespace Umbraco.Core.Persistence.Repositories else { return GetAll(); - //var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - //var allIds = Database.Fetch(sql).ToArray(); - //return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index b864c05a83..ca2b0715ac 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -36,26 +36,11 @@ namespace Umbraco.Core.Persistence.Repositories return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache)); } } - - #region Overrides of RepositoryBase - + protected override IMemberType PerformGet(int id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.NodeId); - - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); - - if (dtos == null || dtos.Any() == false) - return null; - - var factory = new MemberTypeReadOnlyFactory(); - var member = factory.BuildEntity(dtos.First()); - - return member; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -63,10 +48,11 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (ids.Any()) { + //NOTE: This logic should never be executed according to our cache policy var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.id='{0}'", x))); sql.Where(statement); } - sql.OrderByDescending(x => x.NodeId); + sql.OrderByDescending(x => x.NodeId, SqlSyntax); var dtos = Database.Fetch( @@ -82,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories var subquery = translator.Translate(); var sql = GetBaseQuery(false) .Append(new Sql("WHERE umbracoNode.id IN (" + subquery.SQL + ")", subquery.Arguments)) - .OrderBy(x => x.SortOrder); + .OrderBy(x => x.SortOrder, SqlSyntax); var dtos = Database.Fetch( @@ -90,11 +76,7 @@ namespace Umbraco.Core.Persistence.Repositories return BuildFromDtos(dtos); } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -168,11 +150,7 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.MemberType); } } - - #endregion - - #region Unit of Work Implementation - + protected override void PersistNewItem(IMemberType entity) { ValidateAlias(entity); @@ -249,8 +227,7 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - #endregion + /// /// Override so we can specify explicit db type's on any property types that are built-in. @@ -288,9 +265,6 @@ namespace Umbraco.Core.Persistence.Repositories else { return GetAll(); - //var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - //var allIds = Database.Fetch(sql).ToArray(); - //return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index f6bcc46d06..88b8e93772 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -179,7 +179,8 @@ namespace Umbraco.Core.Persistence.Repositories using (var p = CachePolicyFactory.CreatePolicy()) { - return p.GetAll(ids, PerformGetAll); + var result = p.GetAll(ids, PerformGetAll); + return result; } } From 7d9ba0e36e7c662795e5c292d9e14492771f51de Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Jan 2016 19:42:06 +0100 Subject: [PATCH 02/31] manually backports changes from 7.4 fixes --- src/Umbraco.Core/Models/ContentType.cs | 6 + .../Repositories/ContentTypeBaseRepository.cs | 484 +++++++++--------- .../Querying/ContentTypeSqlMappingTests.cs | 12 +- 3 files changed, 252 insertions(+), 250 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 355d724fbe..6a13ea48f5 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -53,6 +53,9 @@ namespace Umbraco.Core.Models /// /// Gets or sets the alias of the default Template. + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity /// [IgnoreDataMember] public ITemplate DefaultTemplate @@ -79,6 +82,9 @@ namespace Umbraco.Core.Models /// /// Gets or Sets a list of Templates which are allowed for the ContentType + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity /// [DataMember] public IEnumerable AllowedTemplates diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 2f42919ce3..58a31d7c36 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -562,14 +562,13 @@ AND umbracoNode.id <> @id", } } - public static IEnumerable GetMediaTypes( - TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetMediaTypes( + Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository) - where TRepo : IReadRepository - where TId: struct + where TRepo : IReadRepository { - IDictionary> allParentMediaTypeIds; - var mediaTypes = MapMediaTypes(mediaTypeIds, db, sqlSyntax, out allParentMediaTypeIds) + IDictionary> allParentMediaTypeIds; + var mediaTypes = MapMediaTypes(db, sqlSyntax, out allParentMediaTypeIds) .ToArray(); MapContentTypeChildren(mediaTypes, db, sqlSyntax, contentTypeRepository, allParentMediaTypeIds); @@ -577,16 +576,15 @@ AND umbracoNode.id <> @id", return mediaTypes; } - public static IEnumerable GetContentTypes( - TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetContentTypes( + Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, ITemplateRepository templateRepository) - where TRepo : IReadRepository - where TId : struct + where TRepo : IReadRepository { - IDictionary> allAssociatedTemplates; - IDictionary> allParentContentTypeIds; - var contentTypes = MapContentTypes(contentTypeIds, db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) + IDictionary> allAssociatedTemplates; + IDictionary> allParentContentTypeIds; + var contentTypes = MapContentTypes(db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) .ToArray(); if (contentTypes.Any()) @@ -601,12 +599,11 @@ AND umbracoNode.id <> @id", return contentTypes; } - internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, + internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, - IDictionary> allParentContentTypeIds) - where TRepo : IReadRepository - where TId : struct + IDictionary> allParentContentTypeIds) + where TRepo : IReadRepository { //NOTE: SQL call #2 @@ -628,20 +625,17 @@ AND umbracoNode.id <> @id", var allParentIdsAsArray = allParentContentTypeIds.SelectMany(x => x.Value).Distinct().ToArray(); if (allParentIdsAsArray.Any()) { - var allParentContentTypes = contentTypeRepository.GetAll(allParentIdsAsArray).ToArray(); + var allParentContentTypes = contentTypes.Where(x => allParentIdsAsArray.Contains(x.Id)).ToArray(); + foreach (var contentType in contentTypes) - { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; + { + var entityId = contentType.Id; var parentContentTypes = allParentContentTypes.Where(x => { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var parentEntityId = typeof(TId) == typeof(int) ? x.Id : (object)x.Key; + var parentEntityId = x.Id; - return allParentContentTypeIds[(TId)entityId].Contains((TId)parentEntityId); + return allParentContentTypeIds[entityId].Contains(parentEntityId); }); foreach (var parentContentType in parentContentTypes) { @@ -659,13 +653,12 @@ AND umbracoNode.id <> @id", } - internal static void MapContentTypeTemplates(IContentType[] contentTypes, + internal static void MapContentTypeTemplates(IContentType[] contentTypes, Database db, TRepo contentTypeRepository, ITemplateRepository templateRepository, - IDictionary> associatedTemplates) - where TRepo : IReadRepository - where TId: struct + IDictionary> associatedTemplates) + where TRepo : IReadRepository { if (associatedTemplates == null || associatedTemplates.Any() == false) return; @@ -682,11 +675,9 @@ AND umbracoNode.id <> @id", foreach (var contentType in contentTypes) { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; - - var associatedTemplateIds = associatedTemplates[(TId)entityId].Select(x => x.TemplateId) + var entityId = contentType.Id; + + var associatedTemplateIds = associatedTemplates[entityId].Select(x => x.TemplateId) .Distinct() .ToArray(); @@ -698,16 +689,11 @@ AND umbracoNode.id <> @id", } - internal static IEnumerable MapMediaTypes(TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> parentMediaTypeIds) - where TId : struct + internal static IEnumerable MapMediaTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> parentMediaTypeIds) { - Mandate.That(mediaTypeIds.Any(), () => new InvalidOperationException("must be at least one content type id specified")); Mandate.ParameterNotNull(db, "db"); - - //ensure they are unique - mediaTypeIds = mediaTypeIds.Distinct().ToArray(); - + 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, @@ -732,27 +718,10 @@ AND umbracoNode.id <> @id", ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" ) ParentTypes ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - - if (mediaTypeIds.Any()) - { - //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will - // work for the time being. - if (typeof(TId) == typeof(int)) - { - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; - } - else if (typeof(TId) == typeof(Guid)) - { - sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; - } - } - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - if ((mediaTypeIds.Length - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType), contentTypeIds = mediaTypeIds }); + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); if (result.Any() == false) { @@ -760,87 +729,110 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentMediaTypeIds = new Dictionary>(); + parentMediaTypeIds = new Dictionary>(); var mappedMediaTypes = new List(); - foreach (var contentTypeId in mediaTypeIds) + //loop through each result and fill in our required values, each row will contain different requried data than the rest. + // it is much quicker to iterate each result and populate instead of looking up the values over and over in the result like + // we used to do. + var queue = new Queue(result); + var currAllowedContentTypes = new List(); + + while (queue.Count > 0) { - //the current content type id that we're working with + var ct = queue.Dequeue(); - var currentCtId = contentTypeId; - - //first we want to get the main content type data this is 1 : 1 with umbraco node data - - var ct = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof (TId) == typeof (int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) - .DistinctBy(x => (int)x.ctId) - .FirstOrDefault(); - - if (ct == null) + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) { - continue; + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) + { + currAllowedContentTypes.Add(ctSort); + } } - var contentTypeDto = new ContentTypeDto + //always ensure there's a list for this content type + if (parentMediaTypeIds.ContainsKey(ct.ctId) == false) + parentMediaTypeIds[ct.ctId] = new List(); + + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) { - Alias = ct.ctAlias, - AllowAtRoot = ct.ctAllowAtRoot, - Description = ct.ctDesc, - Icon = ct.ctIcon, - IsContainer = ct.ctIsContainer, - NodeId = ct.ctId, - PrimaryKey = ct.ctPk, - Thumbnail = ct.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = ct.nCreateDate, - Level = (short)ct.nLevel, - NodeId = ct.ctId, - NodeObjectType = ct.nObjectType, - ParentId = ct.nParentId, - Path = ct.nPath, - SortOrder = ct.nSortOrder, - Text = ct.nName, - Trashed = ct.nTrashed, - UniqueId = ct.nUniqueId, - UserId = ct.nUser - } - }; + var associatedParentIds = parentMediaTypeIds[ct.ctId]; + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } - //now create the media type object + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var mediaType = CreateForMapping(ct, currAllowedContentTypes); + mappedMediaTypes.Add(mediaType); - var factory = new MediaTypeFactory(new Guid(Constants.ObjectTypes.MediaType)); - var mediaType = factory.BuildEntity(contentTypeDto); - - //map the allowed content types - //map the child content type ids - MapCommonContentTypeObjects(mediaType, currentCtId, result, parentMediaTypeIds); - - mappedMediaTypes.Add(mediaType); + //Here we need to reset the current variables, we're now collecting data for a different content type + currAllowedContentTypes = new List(); + } } return mappedMediaTypes; } - internal static IEnumerable MapContentTypes(TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> associatedTemplates, - out IDictionary> parentContentTypeIds) - where TId : struct + private static IMediaType CreateForMapping(dynamic currCt, List currAllowedContentTypes) + { + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list + + var contentTypeDto = new ContentTypeDto + { + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }; + + //now create the content type object + + var factory = new MediaTypeFactory(new Guid(Constants.ObjectTypes.MediaType)); + var mediaType = factory.BuildEntity(contentTypeDto); + + //map the allowed content types + mediaType.AllowedContentTypes = currAllowedContentTypes; + + return mediaType; + } + + internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> associatedTemplates, + out IDictionary> parentContentTypeIds) { Mandate.ParameterNotNull(db, "db"); - - //ensure they are unique - contentTypeIds = contentTypeIds.Distinct().ToArray(); - + 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, @@ -875,28 +867,10 @@ AND umbracoNode.id <> @id", ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" ) ParentTypes ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - - if (contentTypeIds.Any()) - { - //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will - // work for the time being. - if (typeof(TId) == typeof(int)) - { - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; - } - else if (typeof(TId) == typeof(Guid)) - { - sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; - } - } - - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - if ((contentTypeIds.Length - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType), contentTypeIds = contentTypeIds }); + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType)}); if (result.Any() == false) { @@ -905,121 +879,141 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentContentTypeIds = new Dictionary>(); - associatedTemplates = new Dictionary>(); + parentContentTypeIds = new Dictionary>(); + associatedTemplates = new Dictionary>(); var mappedContentTypes = new List(); - foreach (var contentTypeId in contentTypeIds) + var queue = new Queue(result); + var currDefaultTemplate = -1; + var currAllowedContentTypes = new List(); + while (queue.Count > 0) { - //the current content type id that we're working with + var ct = queue.Dequeue(); - var currentCtId = contentTypeId; - - //first we want to get the main content type data this is 1 : 1 with umbraco node data - - var ct = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) - .DistinctBy(x => (int)x.ctId) - .FirstOrDefault(); - - if (ct == null) + //check for default templates + bool? isDefaultTemplate = Convert.ToBoolean(ct.dtIsDefault); + int? templateId = ct.dtTemplateId; + if (currDefaultTemplate == -1 && isDefaultTemplate.HasValue && templateId.HasValue) { - continue; + currDefaultTemplate = templateId.Value; } - //get the unique list of associated templates - var defaultTemplates = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - //use a tuple so that distinct checks both values (in some rare cases the dtIsDefault will not compute as bool?, so we force it with Convert.ToBoolean) - .Select(x => new Tuple(Convert.ToBoolean(x.dtIsDefault), x.dtTemplateId)) - .Where(x => x.Item1.HasValue && x.Item2.HasValue) - .Distinct() - .OrderByDescending(x => x.Item1.Value) - .ToArray(); - //if there isn't one set to default explicitly, we'll pick the first one - var defaultTemplate = defaultTemplates.FirstOrDefault(x => x.Item1.Value) - ?? defaultTemplates.FirstOrDefault(); + //always ensure there's a list for this content type + if (associatedTemplates.ContainsKey(ct.ctId) == false) + associatedTemplates[ct.ctId] = new List(); - var dtDto = new DocumentTypeDto + //check for associated templates and assign to the outgoing collection + if (ct.tId != null) { - //create the content type dto - ContentTypeDto = new ContentTypeDto + var associatedTemplate = new AssociatedTemplate(ct.tId, ct.tAlias, ct.tText); + var associatedList = associatedTemplates[ct.ctId]; + + if (associatedList.Contains(associatedTemplate) == false) + associatedList.Add(associatedTemplate); + } + + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) + { + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) { - Alias = ct.ctAlias, - AllowAtRoot = ct.ctAllowAtRoot, - Description = ct.ctDesc, - Icon = ct.ctIcon, - IsContainer = ct.ctIsContainer, - NodeId = ct.ctId, - PrimaryKey = ct.ctPk, - Thumbnail = ct.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = ct.nCreateDate, - Level = (short)ct.nLevel, - NodeId = ct.ctId, - NodeObjectType = ct.nObjectType, - ParentId = ct.nParentId, - Path = ct.nPath, - SortOrder = ct.nSortOrder, - Text = ct.nName, - Trashed = ct.nTrashed, - UniqueId = ct.nUniqueId, - UserId = ct.nUser - } - }, - ContentTypeNodeId = ct.ctId, - IsDefault = defaultTemplate != null, - TemplateNodeId = defaultTemplate != null ? defaultTemplate.Item2.Value : 0, - }; + currAllowedContentTypes.Add(ctSort); + } + } - // We will map a subset of the associated template - alias, id, name + //always ensure there's a list for this content type + if (parentContentTypeIds.ContainsKey(ct.ctId) == false) + parentContentTypeIds[ct.ctId] = new List(); - associatedTemplates.Add(currentCtId, result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Where(x => x.tId != null) - .Select(x => new AssociatedTemplate(x.tId, x.tAlias, x.tText)) - .Distinct() - .ToArray()); + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) + { + var associatedParentIds = parentContentTypeIds[ct.ctId]; - //now create the content type object + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } - var factory = new ContentTypeFactory(new Guid(Constants.ObjectTypes.DocumentType)); - var contentType = factory.BuildEntity(dtDto); + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var contentType = CreateForMapping(ct, currAllowedContentTypes, currDefaultTemplate); + mappedContentTypes.Add(contentType); - //map the allowed content types - //map the child content type ids - MapCommonContentTypeObjects(contentType, currentCtId, result, parentContentTypeIds); - - mappedContentTypes.Add(contentType); + //Here we need to reset the current variables, we're now collecting data for a different content type + currDefaultTemplate = -1; + currAllowedContentTypes = new List(); + } } return mappedContentTypes; } + private static IContentType CreateForMapping(dynamic currCt, List currAllowedContentTypes, int currDefaultTemplate) + { + // * set the default template to the first one if a default isn't found + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list + + var dtDto = new DocumentTypeDto + { + //create the content type dto + ContentTypeDto = new ContentTypeDto + { + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }, + ContentTypeNodeId = currCt.ctId, + IsDefault = currDefaultTemplate != -1, + TemplateNodeId = currDefaultTemplate != -1 ? currDefaultTemplate : 0, + }; + + //now create the content type object + + var factory = new ContentTypeFactory(new Guid(Constants.ObjectTypes.DocumentType)); + var contentType = factory.BuildEntity(dtDto); + + // NOTE + // that was done by the factory but makes little sense, moved here, so + // now we have to reset dirty props again (as the factory does it) and yet, + // we are not managing allowed templates... the whole thing is weird. + ((ContentType)contentType).DefaultTemplateId = dtDto.TemplateNodeId; + contentType.ResetDirtyProperties(false); + + //map the allowed content types + contentType.AllowedContentTypes = currAllowedContentTypes; + + return contentType; + } + private static void MapCommonContentTypeObjects(T contentType, TId currentCtId, List result, IDictionary> parentContentTypeIds) where T : IContentTypeBase where TId : struct diff --git a/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs b/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs index f63eea7c88..2608f84ae9 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ContentTypeSqlMappingTests.cs @@ -55,10 +55,11 @@ namespace Umbraco.Tests.Persistence.Querying transaction.Complete(); } - IDictionary> allAssociatedTemplates; - IDictionary> allParentContentTypeIds; + IDictionary> allAssociatedTemplates; + IDictionary> allParentContentTypeIds; var contentTypes = ContentTypeRepository.ContentTypeQueryMapper.MapContentTypes( - new[] {99997, 99998}, DatabaseContext.Database, SqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) + DatabaseContext.Database, SqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) + .Where(x => (new[] {99997, 99998}).Contains(x.Id)) .ToArray(); var contentType1 = contentTypes.SingleOrDefault(x => x.Id == 99997); @@ -109,9 +110,10 @@ namespace Umbraco.Tests.Persistence.Querying transaction.Complete(); } - IDictionary> allParentContentTypeIds; + IDictionary> allParentContentTypeIds; var contentTypes = ContentTypeRepository.ContentTypeQueryMapper.MapMediaTypes( - new[] { 99997, 99998 }, DatabaseContext.Database, SqlSyntax, out allParentContentTypeIds) + DatabaseContext.Database, SqlSyntax, out allParentContentTypeIds) + .Where(x => (new[] { 99997, 99998 }).Contains(x.Id)) .ToArray(); var contentType1 = contentTypes.SingleOrDefault(x => x.Id == 99997); From 9a830e5de61cf70fcbe77ecb3fa32c3f89fa1278 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Jan 2016 19:44:36 +0100 Subject: [PATCH 03/31] fixes tests --- .../Cache/FullDataSetRepositoryCachePolicy.cs | 7 +++++-- .../Cache/FullDataSetRepositoryCachePolicyFactory.cs | 11 +++++++---- .../Persistence/Repositories/ContentTypeRepository.cs | 2 +- .../Persistence/Repositories/DomainRepository.cs | 2 +- .../Persistence/Repositories/LanguageRepository.cs | 2 +- .../Persistence/Repositories/MediaTypeRepository.cs | 2 +- .../Persistence/Repositories/MemberTypeRepository.cs | 2 +- .../Repositories/PublicAccessRepository.cs | 2 +- .../Persistence/Repositories/RepositoryBase.cs | 6 +++++- .../Persistence/Repositories/TemplateRepository.cs | 2 +- .../Cache/FullDataSetCachePolicyTests.cs | 8 ++++---- 11 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index b435615d54..c098af8992 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -18,7 +18,9 @@ namespace Umbraco.Core.Cache internal class FullDataSetRepositoryCachePolicy : DefaultRepositoryCachePolicy where TEntity : class, IAggregateRoot { - public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache) : base(cache, + private readonly Func _getEntityId; + + 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, @@ -26,6 +28,7 @@ namespace Umbraco.Core.Cache GetAllCacheAllowZeroCount = true }) { + _getEntityId = getEntityId; } private bool? _hasZeroCountCache; @@ -40,7 +43,7 @@ namespace Umbraco.Core.Cache // Now we can just filter by ids if they have been supplied return ids.Any() - ? result.Where(x => ids.Contains((TId) (object) x.Id)).ToArray() + ? result.Where(x => ids.Contains(_getEntityId(x))).ToArray() : result; } diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs index 75bdae7e83..6a79c2b8c2 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs @@ -1,3 +1,4 @@ +using System; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache @@ -11,15 +12,17 @@ namespace Umbraco.Core.Cache where TEntity : class, IAggregateRoot { private readonly IRuntimeCacheProvider _runtimeCache; - - public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache) + private readonly Func _getEntityId; + + public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, Func getEntityId) { - _runtimeCache = runtimeCache; + _runtimeCache = runtimeCache; + _getEntityId = getEntityId; } public virtual IRepositoryCachePolicy CreatePolicy() { - return new FullDataSetRepositoryCachePolicy(_runtimeCache); + return new FullDataSetRepositoryCachePolicy(_runtimeCache, _getEntityId); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 2fdc0e8613..1a09a2206d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -34,7 +34,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 6abab73dd7..563243f12c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -29,7 +29,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index 6fc9bd5ebc..3884eac888 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -30,7 +30,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 9147cead55..4ee5e1a327 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -31,7 +31,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index ca2b0715ac..4bfdbf3c8a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -33,7 +33,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 1086b9cee0..1d8e56190b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -26,7 +26,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 88b8e93772..5534a9ea40 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -82,7 +82,11 @@ namespace Umbraco.Core.Persistence.Repositories { } - + + protected virtual TId GetEntityId(TEntity entity) + { + return (TId)(object)entity.Id; + } /// /// The runtime cache used for this repo by default is the isolated cache for this type diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index acc17b370a..3545bc1c55 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -51,7 +51,7 @@ 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)); + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory(RuntimeCache, GetEntityId)); } } diff --git a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs index 3d3884c686..9187fe5b27 100644 --- a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Cache return cached.Any() ? new DeepCloneableList() : null; }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); using (defaultPolicy) { var found = defaultPolicy.GetAll(new object[] {}, o => new AuditItem[] {}); @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Cache Assert.IsNotNull(list); //Do it again, ensure that its coming from the cache! - defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object); + defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); using (defaultPolicy) { var found = defaultPolicy.GetAll(new object[] { }, o => new AuditItem[] { }); @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Cache }); cache.Setup(x => x.GetCacheItem(It.IsAny())).Returns(new AuditItem[] { }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); using (defaultPolicy) { var found = defaultPolicy.GetAll(new object[] { }, o => new[] @@ -98,7 +98,7 @@ namespace Umbraco.Tests.Cache new AuditItem(2, "blah2", AuditType.Copy, 123) }); - var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object); + var defaultPolicy = new FullDataSetRepositoryCachePolicy(cache.Object, item => item.Id); using (defaultPolicy) { var found = defaultPolicy.GetAll(new object[] { }, o => new[] { (AuditItem)null }); From e9122385742bcc2865bad3b5ecb7689fda438d19 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 09:32:01 +0100 Subject: [PATCH 04/31] Saw there was another place with the incorrect case for the column name AllowedId --- .../Persistence/Repositories/ContentTypeBaseRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 2f42919ce3..64617c92f2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -710,7 +710,7 @@ AND umbracoNode.id <> @id", 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, @@ -844,7 +844,7 @@ AND umbracoNode.id <> @id", 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, @@ -1242,4 +1242,4 @@ AND umbracoNode.id <> @id", return PerformExists(id); } } -} \ No newline at end of file +} From 81a99d6f7dd893d09ea1556419279b97483a3bbd Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 10:19:51 +0100 Subject: [PATCH 05/31] adds null check and ensures that SetInternalRedirectPublishedContent cannot be called with a null result --- src/Umbraco.Web/Routing/PublishedContentRequest.cs | 3 ++- src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 78f4449c88..02ffc475d3 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -166,7 +166,8 @@ namespace Umbraco.Web.Routing /// preserve or reset the template, if any. public void SetInternalRedirectPublishedContent(IPublishedContent content) { - EnsureWriteable(); + if (content == null) throw new ArgumentNullException("content"); + EnsureWriteable(); // unless a template has been set already by the finder, // template should be null at that point. diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index 65371ecba4..2e075d8ed1 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -515,11 +515,11 @@ namespace Umbraco.Web.Routing { // redirect to another page var node = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); - - _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + if (node != null) { - redirect = true; + _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + redirect = true; ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); } else From a0036d925ed6e2d80cf23898651f8461ac78a674 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 12:14:30 +0100 Subject: [PATCH 06/31] U4-7682 Add option to DatabaseServerMessengerOptions to force a Cold Boot if there are too many instructions --- .../Sync/DatabaseServerMessenger.cs | 24 +++++ .../Sync/DatabaseServerMessengerOptions.cs | 10 +- src/Umbraco.Web/WebBootManager.cs | 94 ++++++++++++------- 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index a4b4d35d10..f74c45948f 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -157,6 +157,30 @@ namespace Umbraco.Core.Sync foreach (var callback in _options.InitializingCallbacks) callback(); } + 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}, proceeding to cold boot", + () => count, () => _options.MaxProcessingInstructionCount); + + // 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 + var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); + if (lastId > 0) + SaveLastSynced(lastId); + + // execute initializing callbacks + if (_options.InitializingCallbacks != null) + foreach (var callback in _options.InitializingCallbacks) + callback(); + + } + } _initialized = true; } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs index f1bebce10b..37a463e2d7 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -2,7 +2,8 @@ using System; using System.Collections.Generic; namespace Umbraco.Core.Sync -{ +{ + /// /// Provides options to the . /// @@ -15,8 +16,15 @@ namespace Umbraco.Core.Sync { DaysToRetainInstructions = 2; // 2 days ThrottleSeconds = 5; // 5 seconds + + MaxProcessingInstructionCount = 1000; } + /// + /// If the number of instructions exceeds this amount during startup then the server will cold boot (rebuild it's own 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.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index a9495afa7c..497db6d17a 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -12,6 +12,7 @@ using System.Web.Routing; using ClientDependency.Core.Config; using Examine; using Examine.Config; +using Examine.Providers; using umbraco; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -57,7 +58,7 @@ namespace Umbraco.Web { private readonly bool _isForTesting; //NOTE: see the Initialize method for what this is used for - private readonly List _indexesToRebuild = new List(); + private static readonly List IndexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication) @@ -210,9 +211,9 @@ namespace Umbraco.Web // (see the initialize method for notes) - we'll ensure we remove the event handler too in case examine manager doesn't actually // initialize during startup, in which case we want it to rebuild the indexes itself. ExamineManager.Instance.BuildingEmptyIndexOnStartup -= OnInstanceOnBuildingEmptyIndexOnStartup; - if (_indexesToRebuild.Any()) + if (IndexesToRebuild.Any()) { - foreach (var indexer in _indexesToRebuild) + foreach (var indexer in IndexesToRebuild) { indexer.RebuildIndex(); } @@ -403,9 +404,9 @@ namespace Umbraco.Web else { - // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed - // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty - // this callback is used below for the DatabaseServerMessenger startup options + //We are using a custom action here so we can check the examine settings value first, we don't want to + // put that check into the CreateIndexesOnColdBoot method because developers may choose to use this + // method directly and they will be in charge of this check if they need it Action rebuildIndexes = () => { //If the developer has explicitly opted out of rebuilding indexes on startup then we @@ -413,39 +414,30 @@ namespace Umbraco.Web // out of sync if they are auto-scaling but there's not much we can do about that. if (ExamineSettings.Instance.RebuildOnAppStart == false) return; - if (_indexesToRebuild.Any()) + foreach (var indexer in GetIndexesForColdBoot()) { - var otherIndexes = ExamineManager.Instance.IndexProviderCollection.Except(_indexesToRebuild); - foreach (var otherIndex in otherIndexes) - { - otherIndex.RebuildIndex(); - } - } - else - { - //rebuild them all - ExamineManager.Instance.RebuildIndex(); + indexer.RebuildIndex(); } }; ServerMessengerResolver.Current.SetServerMessenger(new BatchedDatabaseServerMessenger( - ApplicationContext, - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] + ApplicationContext, + true, + //Default options for web including the required callbacks to build caches + new DatabaseServerMessengerOptions { - //rebuild the xml cache file if the server is not synced - () => global::umbraco.content.Instance.RefreshContentFromDatabase(), - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - rebuildIndexes - } - })); + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => global::umbraco.content.Instance.RefreshContentFromDatabase(), + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + rebuildIndexes + } + })); } SurfaceControllerResolver.Current = new SurfaceControllerResolver( @@ -533,12 +525,46 @@ namespace Umbraco.Web new DefaultCultureDictionaryFactory()); } + /// + /// The method used to create indexes on a cold boot + /// + /// + /// A cold boot is when the server determines it will not (or cannot) process instructions in the cache table and + /// will rebuild it's own caches itself. + /// + public static IEnumerable GetIndexesForColdBoot() + { + // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed + // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty + // this callback is used below for the DatabaseServerMessenger startup options + + if (IndexesToRebuild.Any()) + { + var otherIndexes = ExamineManager.Instance.IndexProviderCollection.Cast().Except(IndexesToRebuild); + + foreach (var otherIndex in otherIndexes) + { + yield return otherIndex; + } + } + else + { + foreach (var index in ExamineManager.Instance.IndexProviderCollection.Cast()) + { + yield return index; + } + } + + IndexesToRebuild.Clear(); + } + + private void OnInstanceOnBuildingEmptyIndexOnStartup(object sender, BuildingEmptyIndexOnStartupEventArgs args) { //store the indexer that needs rebuilding because it's empty for when the boot process // is complete and cancel this current event so the rebuild process doesn't start right now. args.Cancel = true; - _indexesToRebuild.Add(args.Indexer); + IndexesToRebuild.Add((BaseIndexProvider)args.Indexer); } } } From ec815837415e4012af6542fd96c19f7a51dfb608 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 13:50:45 +0100 Subject: [PATCH 07/31] Fixes case sensitive matching on alias in TemplateRepository --- .../Repositories/TemplateRepository.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index acc17b370a..0433340599 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -488,7 +488,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 +497,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 +532,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 +552,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 +573,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 +582,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 +598,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 +730,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 +760,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; } From 1fcea083e76b875d4b3a3592cddd455b8ef64bc9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 14:10:13 +0100 Subject: [PATCH 08/31] Ensures no profiling occurs when not in debug mode --- src/Umbraco.Core/Profiling/WebProfiler.cs | 37 ++++++++++++++--------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Profiling/WebProfiler.cs b/src/Umbraco.Core/Profiling/WebProfiler.cs index 7e2cf49313..45d3a68591 100644 --- a/src/Umbraco.Core/Profiling/WebProfiler.cs +++ b/src/Umbraco.Core/Profiling/WebProfiler.cs @@ -19,18 +19,21 @@ namespace Umbraco.Core.Profiling /// internal WebProfiler() { - //setup some defaults - MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); - MiniProfiler.Settings.StackMaxLength = 5000; + if (GlobalSettings.DebugMode) + { + //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; + //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; + //Binds to application events to enable the MiniProfiler with a real HttpRequest + UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; + } } /// @@ -123,7 +126,7 @@ namespace Umbraco.Core.Profiling /// public string Render() { - return MiniProfiler.RenderIncludes(RenderPosition.Right).ToString(); + return GlobalSettings.DebugMode ? MiniProfiler.RenderIncludes(RenderPosition.Right).ToString() : string.Empty; } /// @@ -143,8 +146,11 @@ namespace Umbraco.Core.Profiling /// Start the profiler /// public void Start() - { - MiniProfiler.Start(); + { + if (GlobalSettings.DebugMode) + { + MiniProfiler.Start(); + } } /// @@ -156,7 +162,10 @@ namespace Umbraco.Core.Profiling /// public void Stop(bool discardResults = false) { - MiniProfiler.Stop(discardResults); + if (GlobalSettings.DebugMode) + { + MiniProfiler.Stop(discardResults); + } } /// From c9c451be65ec552edc55921871d8f03ea2bd87b6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 28 Jan 2016 14:19:32 +0100 Subject: [PATCH 09/31] U4-7682 Minor review adjustments --- .../Sync/DatabaseServerMessenger.cs | 72 ++++++++-------- .../Sync/DatabaseServerMessengerOptions.cs | 6 +- src/Umbraco.Web/WebBootManager.cs | 82 +++++++++---------- 3 files changed, 76 insertions(+), 84 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index f74c45948f..8a4d79725a 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 @@ -157,30 +180,6 @@ namespace Umbraco.Core.Sync foreach (var callback in _options.InitializingCallbacks) callback(); } - 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}, proceeding to cold boot", - () => count, () => _options.MaxProcessingInstructionCount); - - // 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 - var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); - if (lastId > 0) - SaveLastSynced(lastId); - - // execute initializing callbacks - if (_options.InitializingCallbacks != null) - foreach (var callback in _options.InitializingCallbacks) - callback(); - - } - } _initialized = true; } @@ -193,7 +192,7 @@ namespace Umbraco.Core.Sync { lock (_locko) { - if (_syncing) + if (_syncing) return; if (_released) @@ -237,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? @@ -305,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 /// @@ -314,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. @@ -355,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. /// @@ -526,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 37a463e2d7..7559c37813 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; namespace Umbraco.Core.Sync { - /// /// Provides options to the . /// @@ -15,13 +14,12 @@ namespace Umbraco.Core.Sync public DatabaseServerMessengerOptions() { DaysToRetainInstructions = 2; // 2 days - ThrottleSeconds = 5; // 5 seconds - + ThrottleSeconds = 5; // 5 second MaxProcessingInstructionCount = 1000; } /// - /// If the number of instructions exceeds this amount during startup then the server will cold boot (rebuild it's own caches) + /// The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches). /// public int MaxProcessingInstructionCount { get; set; } diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 497db6d17a..3ca8903c98 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -52,13 +52,13 @@ using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; namespace Umbraco.Web { /// - /// A bootstrapper for the Umbraco application which initializes all objects including the Web portion of the application + /// A bootstrapper for the Umbraco application which initializes all objects including the Web portion of the application /// public class WebBootManager : CoreBootManager { private readonly bool _isForTesting; //NOTE: see the Initialize method for what this is used for - private static readonly List IndexesToRebuild = new List(); + private static readonly List IndexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication) @@ -105,7 +105,7 @@ namespace Umbraco.Web public override IBootManager Initialize() { //This is basically a hack for this item: http://issues.umbraco.org/issue/U4-5976 - // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's + // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's // event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build // it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the // boot process has completed. It's a hack but it works. @@ -146,7 +146,7 @@ namespace Umbraco.Web { "compositeFileHandlerPath", ClientDependencySettings.Instance.CompositeFileHandlerPath } }); ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); - + // Disable the X-AspNetMvc-Version HTTP Header MvcHandler.DisableMvcResponseHeader = true; @@ -155,9 +155,9 @@ namespace Umbraco.Web return this; } - + /// - /// Override this method in order to ensure that the UmbracoContext is also created, this can only be + /// Override this method in order to ensure that the UmbracoContext is also created, this can only be /// created after resolution is frozen! /// protected override void FreezeResolution() @@ -171,8 +171,8 @@ namespace Umbraco.Web httpContext, ApplicationContext, new WebSecurity(httpContext, ApplicationContext), - UmbracoConfig.For.UmbracoSettings(), - UrlProviderResolver.Current.Providers, + UmbracoConfig.For.UmbracoSettings(), + UrlProviderResolver.Current.Providers, false); } @@ -188,7 +188,7 @@ namespace Umbraco.Web ProfilerResolver.Current.SetProfiler(profiler); profiler.Start(); } - + /// /// Ensure that the OnApplicationStarted methods of the IApplicationEvents are called /// @@ -206,8 +206,8 @@ namespace Umbraco.Web //Now, startup all of our legacy startup handler ApplicationEventsResolver.Current.InstantiateLegacyStartupHandlers(); - - //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them + + //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them // (see the initialize method for notes) - we'll ensure we remove the event handler too in case examine manager doesn't actually // initialize during startup, in which case we want it to rebuild the indexes itself. ExamineManager.Instance.BuildingEmptyIndexOnStartup -= OnInstanceOnBuildingEmptyIndexOnStartup; @@ -245,14 +245,14 @@ namespace Umbraco.Web { //create a web-based cache helper var cacheHelper = new CacheHelper( - //we need to have the dep clone runtime cache provider to ensure + //we need to have the dep clone runtime cache provider to ensure //all entities are cached properly (cloned in and cloned out) new DeepCloneRuntimeCacheProvider(new HttpRuntimeCacheProvider(HttpRuntime.Cache)), new StaticCacheProvider(), //we have no request based cache when not running in web-based context new NullCacheProvider(), new IsolatedRuntimeCache(type => - //we need to have the dep clone runtime cache provider to ensure + //we need to have the dep clone runtime cache provider to ensure //all entities are cached properly (cloned in and cloned out) new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); @@ -283,7 +283,7 @@ namespace Umbraco.Web //plugin controllers must come first because the next route will catch many things RoutePluginControllers(); } - + private void RoutePluginControllers() { var umbracoPath = GlobalSettings.UmbracoMvcArea; @@ -350,14 +350,14 @@ namespace Umbraco.Web umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}",//url to match new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, new[] { meta.ControllerNamespace }); //look in this namespace to create the controller - route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set + route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set route.DataTokens.Add("UseNamespaceFallback", false); //Don't look anywhere else except this namespace! //make it use our custom/special SurfaceMvcHandler route.RouteHandler = new SurfaceRouteHandler(); } /// - /// Initializes all web based and core resolves + /// Initializes all web based and core resolves /// protected override void InitializeResolvers() { @@ -376,7 +376,7 @@ namespace Umbraco.Web //set the legacy one by default - this maintains backwards compat ServerMessengerResolver.Current.SetServerMessenger(new BatchedWebServiceServerMessenger(() => { - //we should not proceed to change this if the app/database is not configured since there will + //we should not proceed to change this if the app/database is not configured since there will // be no user, plus we don't need to have server messages sent if this is the case. if (ApplicationContext.IsConfigured && ApplicationContext.DatabaseContext.IsDatabaseConfigured) { @@ -404,12 +404,12 @@ namespace Umbraco.Web else { - //We are using a custom action here so we can check the examine settings value first, we don't want to - // put that check into the CreateIndexesOnColdBoot method because developers may choose to use this + //We are using a custom action here so we can check the examine settings value first, we don't want to + // put that check into the CreateIndexesOnColdBoot method because developers may choose to use this // method directly and they will be in charge of this check if they need it Action rebuildIndexes = () => { - //If the developer has explicitly opted out of rebuilding indexes on startup then we + //If the developer has explicitly opted out of rebuilding indexes on startup then we // should adhere to that and not do it, this means that if they are load balancing things will be // out of sync if they are auto-scaling but there's not much we can do about that. if (ExamineSettings.Instance.RebuildOnAppStart == false) return; @@ -433,8 +433,8 @@ namespace Umbraco.Web //rebuild the xml cache file if the server is not synced () => global::umbraco.content.Instance.RefreshContentFromDatabase(), //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. rebuildIndexes } })); @@ -462,7 +462,7 @@ namespace Umbraco.Web new PublishedCache.XmlPublishedCache.PublishedContentCache(), new PublishedCache.XmlPublishedCache.PublishedMediaCache(ApplicationContext))); - GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), + GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration)); FilteredControllerFactoriesResolver.Current = new FilteredControllerFactoriesResolver( @@ -527,41 +527,37 @@ namespace Umbraco.Web /// /// The method used to create indexes on a cold boot - /// + /// /// /// A cold boot is when the server determines it will not (or cannot) process instructions in the cache table and /// will rebuild it's own caches itself. /// public static IEnumerable GetIndexesForColdBoot() { - // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed - // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty - // this callback is used below for the DatabaseServerMessenger startup options - + // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed + // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty. + // This callback is used above for the DatabaseServerMessenger startup options. + + // all indexes + IEnumerable indexes = ExamineManager.Instance.IndexProviderCollection; + + // except those that are already flagged + // and are processed in Complete() if (IndexesToRebuild.Any()) - { - var otherIndexes = ExamineManager.Instance.IndexProviderCollection.Cast().Except(IndexesToRebuild); + indexes = indexes.Except(IndexesToRebuild); - foreach (var otherIndex in otherIndexes) - { - yield return otherIndex; - } - } - else - { - foreach (var index in ExamineManager.Instance.IndexProviderCollection.Cast()) - { - yield return index; - } - } + // return + foreach (var index in indexes) + yield return index; + // and clear IndexesToRebuild.Clear(); } private void OnInstanceOnBuildingEmptyIndexOnStartup(object sender, BuildingEmptyIndexOnStartupEventArgs args) { - //store the indexer that needs rebuilding because it's empty for when the boot process + //store the indexer that needs rebuilding because it's empty for when the boot process // is complete and cancel this current event so the rebuild process doesn't start right now. args.Cancel = true; IndexesToRebuild.Add((BaseIndexProvider)args.Indexer); From 256849f8a65a48c178dcbb7cfcd78289a61f2432 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 14:19:38 +0100 Subject: [PATCH 10/31] adds nicer profiling during startup for app event handlers. --- src/Umbraco.Core/CoreBootManager.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 17f909da8a..f8dd255095 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) { From a6093f41bd2b5b490dd497f56203200ee374f181 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 18:02:22 +0100 Subject: [PATCH 11/31] Fixes installer login for user and adds notes --- src/Umbraco.Core/Services/ContentService.cs | 7 ++++++ .../Services/ContentTypeService.cs | 7 ++++++ .../InstallSteps/SetUmbracoVersionStep.cs | 25 +++++++++++-------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 2edc32f367..7c34709787 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 e6ead68810..1401659bf3 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -452,6 +452,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.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index f7c9b3d6a5..78fe6d3766 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -27,6 +27,20 @@ namespace Umbraco.Web.Install.InstallSteps public override InstallSetupResult Execute(object model) { + var ih = new InstallHelper(UmbracoContext.Current); + + //During a new install we'll log the default user in (which is id = 0). + // During an upgrade, the user will already need to be logged in in order to run the installer. + + var security = new WebSecurity(_httpContext, _applicationContext); + //we do this check here because for upgrades the user will already be logged in, for brand new installs, + // they will not be logged in, however we cannot check the current installation status because it will tell + // us that it is in 'upgrade' because we already have a database conn configured and a database. + if (security.IsAuthenticated() == false && GlobalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) + { + security.PerformLogin(0); + } + //This is synonymous with library.RefreshContent() - but we don't want to use library // for anything anymore so welll use the method that it is wrapping. This will just make sure // the correct xml structure exists in the xml cache file. This is required by some upgrade scripts @@ -39,17 +53,8 @@ namespace Umbraco.Web.Install.InstallSteps // Update ClientDependency version var clientDependencyConfig = new ClientDependencyConfiguration(_applicationContext.ProfilingLogger.Logger); var clientDependencyUpdated = clientDependencyConfig.IncreaseVersionNumber(); - - //During a new install we'll log the default user in (which is id = 0). - // During an upgrade, the user will already need to be logged in in order to run the installer. - if (InstallTypeTarget == InstallationType.NewInstall) - { - var security = new WebSecurity(_httpContext, _applicationContext); - security.PerformLogin(0); - } - //reports the ended install - var ih = new InstallHelper(UmbracoContext.Current); + //reports the ended install ih.InstallStatus(true, ""); return null; From 6faa7e2fc91edc14a0136a7357652610ea8e52df Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 18:31:47 +0100 Subject: [PATCH 12/31] dont' have RebuildOnAppStart set for dev purposes! --- src/Umbraco.Web.UI/config/ExamineSettings.config | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 - From 8916c68a0c00896c2d20e170d329a419e39a59ea Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 18:35:29 +0100 Subject: [PATCH 13/31] reverts profiler changes, we'll inlcude in 7.4 --- .../Profiling/StartupWebProfilerProvider.cs | 126 ------------------ src/Umbraco.Core/Profiling/WebProfiler.cs | 48 ++----- src/Umbraco.Core/Umbraco.Core.csproj | 1 - 3 files changed, 11 insertions(+), 164 deletions(-) delete mode 100644 src/Umbraco.Core/Profiling/StartupWebProfilerProvider.cs 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 45d3a68591..00d088bca7 100644 --- a/src/Umbraco.Core/Profiling/WebProfiler.cs +++ b/src/Umbraco.Core/Profiling/WebProfiler.cs @@ -12,28 +12,16 @@ namespace Umbraco.Core.Profiling /// internal class WebProfiler : IProfiler { - private StartupWebProfilerProvider _startupWebProfilerProvider; /// /// Constructor - /// + /// + /// + /// Binds to application events to enable the MiniProfiler + /// internal WebProfiler() { - if (GlobalSettings.DebugMode) - { - //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; - } + UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; } /// @@ -65,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(); } @@ -83,11 +66,6 @@ namespace Umbraco.Core.Profiling /// void UmbracoApplicationBeginRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) - { - _startupWebProfilerProvider.BootComplete(); - } - if (CanPerformProfilingAction(sender)) { Start(); @@ -126,7 +104,7 @@ namespace Umbraco.Core.Profiling /// public string Render() { - return GlobalSettings.DebugMode ? MiniProfiler.RenderIncludes(RenderPosition.Right).ToString() : string.Empty; + return MiniProfiler.RenderIncludes(RenderPosition.Right).ToString(); } /// @@ -147,10 +125,9 @@ namespace Umbraco.Core.Profiling /// public void Start() { - if (GlobalSettings.DebugMode) - { - MiniProfiler.Start(); - } + MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); + MiniProfiler.Settings.StackMaxLength = 5000; + MiniProfiler.Start(); } /// @@ -162,10 +139,7 @@ namespace Umbraco.Core.Profiling /// public void Stop(bool discardResults = false) { - if (GlobalSettings.DebugMode) - { - MiniProfiler.Stop(discardResults); - } + MiniProfiler.Stop(discardResults); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d11270592a..2ef0501f8e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -469,7 +469,6 @@ - From d1be38be64b7e9150a9f993596bebb06f2db145a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2016 18:37:56 +0100 Subject: [PATCH 14/31] reverts profiler changes, we'll inlcude in 7.4 --- src/Umbraco.Web/WebBootManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 3ca8903c98..67e0e65703 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -182,11 +182,8 @@ namespace Umbraco.Web protected override void InitializeProfilerResolver() { base.InitializeProfilerResolver(); - //Set the profiler to be the web profiler - var profiler = new WebProfiler(); - ProfilerResolver.Current.SetProfiler(profiler); - profiler.Start(); + ProfilerResolver.Current.SetProfiler(new WebProfiler()); } /// From 1abab419556fc11e42753b11a304ef5f2a848259 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 1 Feb 2016 21:45:34 +0100 Subject: [PATCH 15/31] Fixes perf issue with DeepCloneHelper - so we cache the actual property types instead of re-reflecting each time --- src/Umbraco.Core/Models/DeepCloneHelper.cs | 160 +++++++++++++-------- 1 file changed, 99 insertions(+), 61 deletions(-) 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); } } From 1dea0edcf18887a55ca41cd9c6714432244c2ed7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 1 Feb 2016 22:50:38 +0100 Subject: [PATCH 16/31] re-includes the static cache for published property types, this cache is much more than a simple cache of content type infos, it is also the cache for associated converters which is required for all front-end rendering. Fixes the issue of not setting the xpath cache level corectly. --- .../PublishedContent/PublishedContentType.cs | 58 +++++++++++++++++-- .../PublishedContent/PublishedPropertyType.cs | 4 +- .../Cache/ContentTypeCacheRefresher.cs | 8 ++- .../Cache/DataTypeCacheRefresher.cs | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) 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.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index c44b3f2b51..44a6efe9ff 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -141,7 +141,9 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.ContentTypeCacheKey); //clear static object cache global::umbraco.cms.businesslogic.ContentType.RemoveAllDataTypeCache(); - + + PublishedContentType.ClearAll(); + base.RefreshAll(); } @@ -278,7 +280,9 @@ namespace Umbraco.Web.Cache //clears the dictionary object cache of the legacy ContentType global::umbraco.cms.businesslogic.ContentType.RemoveFromDataTypeCache(payload.Alias); - + + PublishedContentType.ClearContentType(payload.Id); + //need to recursively clear the cache for each child content type foreach (var descendant in payload.DescendantPayloads) { diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 173f4dcb86..11b3ab6294 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -108,6 +108,7 @@ namespace Umbraco.Web.Cache if (dataTypeCache) dataTypeCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id)); + PublishedContentType.ClearDataType(payload.Id); }); base.Refresh(jsonPayload); From 6e27b3d6d45c4693168cce4a2be868fe5bd75256 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 2 Feb 2016 00:47:18 +0100 Subject: [PATCH 17/31] Fixes up the FullDataSetRepositoryCachePolicy to handle individual items, updates and removes correctly, splits up it's logic so it's not overriding the DefaultRepositoryCachePolicy since that is just different. Adds tests. --- .../Cache/DefaultRepositoryCachePolicy.cs | 78 ++++----- .../Cache/FullDataSetRepositoryCachePolicy.cs | 163 +++++++++++++++--- ...FullDataSetRepositoryCachePolicyFactory.cs | 9 +- .../Cache/IRepositoryCachePolicy.cs | 4 +- .../Cache/RepositoryCachePolicyBase.cs | 48 ++++++ .../SingleItemsOnlyRepositoryCachePolicy.cs | 2 +- .../Repositories/ContentTypeRepository.cs | 5 +- .../Repositories/DomainRepository.cs | 3 +- .../Repositories/LanguageRepository.cs | 3 +- .../Repositories/MediaTypeRepository.cs | 5 +- .../Repositories/MemberTypeRepository.cs | 5 +- .../Repositories/PublicAccessRepository.cs | 3 +- .../Repositories/TemplateRepository.cs | 3 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Cache/DefaultCachePolicyTests.cs | 32 ++++ .../Cache/FullDataSetCachePolicyTests.cs | 154 +++++++++++++++-- 16 files changed, 420 insertions(+), 98 deletions(-) create mode 100644 src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs 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..eeb651dc09 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -11,33 +11,114 @@ 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; + 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()); + + //we don't have anything in cache (this should never happen), just return from the repo + return found == null + ? getFromRepo(id) + : found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + } + + public override TEntity Get(TId id) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo()); + + //we don't have anything in cache (this should never happen), just return null + return found == null + ? null + : found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + } + + public override bool Exists(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] {}, ids => _getAllFromRepo()); + + //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 the base logic without any Ids - we want to cache them all! - var result = base.GetAll(new TId[] { }, 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 @@ -47,31 +128,64 @@ namespace Umbraco.Core.Cache : result; } + protected 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 +193,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 +206,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 +215,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/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 1a09a2206d..1441db0907 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.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)); } } 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 4ee5e1a327..2651cf98d4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -31,7 +31,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/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 4bfdbf3c8a..ddbf08e71b 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 a523a06293..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)); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2ef0501f8e..e0991e9ba7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -175,6 +175,7 @@ + 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..d3df319ac7 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; @@ -36,20 +84,20 @@ namespace Umbraco.Tests.Cache return cached.Any() ? new DeepCloneableList() : 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,7 +139,9 @@ 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 @@ -98,12 +150,88 @@ namespace Umbraco.Tests.Cache 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 From 1db635f24c43a78f4c359de3288b423cbc2c37db Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 2 Feb 2016 01:32:36 +0100 Subject: [PATCH 18/31] Updates DeepCloneableList to support behaviors, for the FullDataSetCachePolicy we only want to clone when writing to cache, not when reading, the cloning will then be done on individual items after filtering by the FullDataSetRepositoryCachePolicy --- .../Cache/FullDataSetRepositoryCachePolicy.cs | 33 +++++---- .../Collections/DeepCloneableList.cs | 71 ++++++++++++++----- .../Collections/ListCloneBehavior.cs | 20 ++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../DeepCloneRuntimeCacheProviderTests.cs | 2 +- .../Cache/FullDataSetCachePolicyTests.cs | 4 +- .../Collections/DeepCloneableListTests.cs | 40 ++++++++++- 7 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Core/Collections/ListCloneBehavior.cs diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs index eeb651dc09..cae7bc16e6 100644 --- a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -85,29 +85,35 @@ namespace Umbraco.Core.Cache public override TEntity Get(TId id, Func getFromRepo) { //Force get all with cache - var found = GetAll(new TId[] { }, ids => _getAllFromRepo()); + 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.FirstOrDefault(x => _getEntityId(x).Equals(id)); + 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()); + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); //we don't have anything in cache (this should never happen), just return null - return found == null - ? null - : found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + 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()); + 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 @@ -123,12 +129,15 @@ namespace Umbraco.Core.Cache //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() + return (ids.Any() ? result.Where(x => ids.Contains(_getEntityId(x))).ToArray() - : result; + : 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(); } - protected TEntity[] PerformGetAll(Func> getFromRepo) + private TEntity[] PerformGetAll(Func> getFromRepo) { var allEntities = GetAllFromCache(); if (allEntities.Any()) 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/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e0991e9ba7..49eaa8a629 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -188,6 +188,7 @@ + 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/FullDataSetCachePolicyTests.cs b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs index d3df319ac7..96e22e3aff 100644 --- a/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs +++ b/src/Umbraco.Tests/Cache/FullDataSetCachePolicyTests.cs @@ -81,7 +81,7 @@ 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, () => getAll, false); @@ -144,7 +144,7 @@ namespace Umbraco.Tests.Cache 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) 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()); From 0af97f63e27a02d5f577409594dcd11a9a5e8253 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 2 Feb 2016 11:11:47 +0100 Subject: [PATCH 19/31] U4-7857 Flexible Load Balancing does not sync with the correct timeout threshold --- src/Umbraco.Core/Sync/DatabaseServerMessenger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 8a4d79725a..87fc694fd1 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -198,7 +198,7 @@ namespace Umbraco.Core.Sync if (_released) return; - if ((DateTime.UtcNow - _lastSync).Seconds <= _options.ThrottleSeconds) + if ((DateTime.UtcNow - _lastSync).TotalSeconds <= _options.ThrottleSeconds) return; _syncing = true; From bd2fc71dc589498fd77b4b46df50c74fa8411fc2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 2 Feb 2016 11:55:24 +0100 Subject: [PATCH 20/31] U4-7276 When creating folders in the "Partials" view folder a YSOD appears (Umbraco 7.3 and 7.4) --- .../umbraco/create/PartialViewTasksBase.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs index 042bf312d1..abb1299507 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/create/PartialViewTasksBase.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Web; using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -52,7 +53,7 @@ namespace umbraco if (IsPartialViewMacro == false) { var attempt = fileService.CreatePartialView(model, snippetName, User.Id); - _returnUrl = string.Format("settings/views/EditView.aspx?treeType=partialViews&file={0}", model.Path.TrimStart('/')); + _returnUrl = string.Format("settings/views/EditView.aspx?treeType=partialViews&file={0}", HttpUtility.UrlEncode(model.Path.TrimStart('/').Replace("\\", "/"))); return attempt.Success; } else @@ -68,7 +69,7 @@ namespace umbraco macroService.Save(new Macro(attempt.Result.Alias, attempt.Result.Alias) { ScriptPath = virtualPath }); } - _returnUrl = string.Format("settings/views/EditView.aspx?treeType=partialViewMacros&file={0}", model.Path.TrimStart('/')); + _returnUrl = string.Format("settings/views/EditView.aspx?treeType=partialViewMacros&file={0}", HttpUtility.UrlEncode(model.Path.TrimStart('/').Replace("\\", "/"))); return attempt.Success; } From 6583ff443973388bc2030769ab26fda550533889 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 2 Feb 2016 12:12:51 +0100 Subject: [PATCH 21/31] U4-7821 KeepUserLoggedIn with a long umbracoTimeOutInMinutes has logout issues --- .../Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs index 2b2ecb4295..188a235c8c 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs @@ -44,7 +44,7 @@ namespace Umbraco.Web.Security.Identity CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; + CookiePath = "/"; //Custom cookie manager so we can filter requests CookieManager = new BackOfficeCookieManager(new SingletonUmbracoContextAccessor(), explicitPaths); @@ -84,7 +84,7 @@ namespace Umbraco.Web.Security.Identity if (ticket.Properties.IsPersistent) { - cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + cookieOptions.Expires = expiresUtc.UtcDateTime; } return cookieOptions; From 98b8aedc4d1f57ccb51637f9c7e20a8402f28923 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 2 Feb 2016 15:13:57 +0100 Subject: [PATCH 22/31] U4-7849 Unclear error message when applying public access --- src/Umbraco.Web.UI.Client/src/less/hacks.less | 93 +++++++++++- .../umbraco/dialogs/protectPage.aspx | 105 +++++++------- .../umbraco/dialogs/protectPage.aspx.cs | 134 +++++++++--------- 3 files changed, 218 insertions(+), 114 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 04da8fb0af..c0e80b4e48 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -67,4 +67,95 @@ iframe, .content-column-body { } .icon-chevron-down:before { content: "\e0c9"; -} \ No newline at end of file +} + + +/* 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/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 @@