From 3fac40b4b5dbadd0e099badc7f6745478b0393e1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 10 Feb 2020 20:15:12 +1100 Subject: [PATCH 01/74] loads preview via sql paging instead of one single query --- .../Repositories/ContentRepository.cs | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index a0b211b6b2..24c9e84873 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -837,6 +837,8 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", public XmlDocument BuildPreviewXmlCache() { + + var xmlDoc = new XmlDocument(); var doctype = xmlDoc.CreateDocumentType("root", null, null, ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); @@ -851,42 +853,61 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 -where umbracoNode.trashed = 0 -order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", +where (umbracoNode.trashed = 0) +order by (umbracoNode.{2}), (umbracoNode.parentID), (umbracoNode.sortOrder)", SqlSyntax.GetQuotedColumnName("xml"), SqlSyntax.GetQuotedColumnName("level"), SqlSyntax.GetQuotedColumnName("level")); + var args = new object[] { new { type = NodeObjectTypeId } }; XmlElement last = null; - //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + long pageSize = 500; + int? itemCount = null; + long currPage = 0; + do { - string parentId = ((int)row.parentID).ToInvariantString(); - string xml = row.xml; - int sortOrder = row.sortOrder; - //if the parentid is changing - if (last != null && last.GetAttribute("parentID") != parentId) + // Get the paged queries + Database.BuildPageQueries(currPage, pageSize, sql, ref args, out var sqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) { - parent = xmlDoc.GetElementById(parentId); - if (parent == null) + itemCount = Database.ExecuteScalar(sqlCount, args); + } + currPage++; + + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (var row in Database.Query(sqlPage, args)) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) { - //Need to short circuit here, if the parent is not there it means that the parent is unpublished - // and therefore the child is not published either so cannot be included in the xml cache - continue; + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); } - var xmlDocFragment = xmlDoc.CreateDocumentFragment(); - xmlDocFragment.InnerXml = xml; - - last = (XmlElement)parent.AppendChild(xmlDocFragment); - - // fix sortOrder - see notes in UpdateSortOrder - last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); - } - + } while (itemCount == pageSize); + return xmlDoc; } From e748d842a48282e74ba47edfdb25efad6a9bdc9a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Mar 2020 22:50:47 +1100 Subject: [PATCH 02/74] Fixes Memory Leak in GetCacheItem cache dependency #7773 --- .../Cache/HttpRuntimeCacheProvider.cs | 59 +++++++------------ src/umbraco.businesslogic/CacheHelper.cs | 31 ---------- .../umbraco.businesslogic.csproj | 1 - 3 files changed, 22 insertions(+), 69 deletions(-) delete mode 100644 src/umbraco.businesslogic/CacheHelper.cs diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index 0e98cd5318..30ce3c9926 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -72,21 +72,16 @@ namespace Umbraco.Core.Cache /// public override object GetCacheItem(string cacheKey, Func getCacheItem) { - return GetCacheItem(cacheKey, getCacheItem, null, dependentFiles: null); + return GetCacheItemInternal(cacheKey, getCacheItem, null); } - /// - /// This overload is here for legacy purposes - /// - /// - /// - /// - /// - /// - /// - /// - /// + [Obsolete("This is here for legacy reasons only, do not use this method, it has memory leaks")] internal object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) + { + return GetCacheItemInternal(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, () => dependency); + } + + private object GetCacheItemInternal(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, Func dependency = null) { cacheKey = GetCacheKey(cacheKey); @@ -141,7 +136,7 @@ namespace Umbraco.Core.Cache lck.UpgradeToWriteLock(); //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! - _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); + _cache.Insert(cacheKey, result, dependency?.Invoke(), absolute, sliding, priority, removedCallback); } } @@ -160,29 +155,22 @@ namespace Umbraco.Core.Cache public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - CacheDependency dependency = null; - if (dependentFiles != null && dependentFiles.Any()) - { - dependency = new CacheDependency(dependentFiles); - } - return GetCacheItem(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, dependency); + return GetCacheItemInternal(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, + // Don't create a CacheDependency object unless we need it, see https://github.com/umbraco/Umbraco-CMS/issues/7773 + () => dependentFiles != null && dependentFiles.Length > 0 ? new CacheDependency(dependentFiles) : null); } #endregion #region Insert - /// - /// This overload is here for legacy purposes - /// - /// - /// - /// - /// - /// - /// - /// + [Obsolete("This is here for legacy reasons only, do not use this method, it has memory leaks")] internal void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) + { + InsertCacheItemInternal(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, () => dependency); + } + + private void InsertCacheItemInternal(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, Func dependency = null) { // NOTE - here also we must insert a Lazy but we can evaluate it right now // and make sure we don't store a null value. @@ -199,20 +187,17 @@ namespace Umbraco.Core.Cache using (new WriteLock(_locker)) { //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! - _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); + _cache.Insert(cacheKey, result, dependency?.Invoke(), absolute, sliding, priority, removedCallback); } } public void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - CacheDependency dependency = null; - if (dependentFiles != null && dependentFiles.Any()) - { - dependency = new CacheDependency(dependentFiles); - } - InsertCacheItem(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, dependency); + InsertCacheItemInternal(cacheKey, getCacheItem, timeout, isSliding, priority, removedCallback, + // Don't create a CacheDependency object unless we need it, see https://github.com/umbraco/Umbraco-CMS/issues/7773 + () => dependentFiles != null && dependentFiles.Length > 0 ? new CacheDependency(dependentFiles) : null); } #endregion } -} \ No newline at end of file +} diff --git a/src/umbraco.businesslogic/CacheHelper.cs b/src/umbraco.businesslogic/CacheHelper.cs deleted file mode 100644 index 243da8eadd..0000000000 --- a/src/umbraco.businesslogic/CacheHelper.cs +++ /dev/null @@ -1,31 +0,0 @@ -//using System; -//using System.Web.Caching; - -//namespace umbraco.BusinessLogic -//{ -// internal class CacheHelper -// { -// public delegate TT GetCacheItemDelegate(); -// public static TT GetCacheItem(string cacheKey, object syncLock, -// CacheItemPriority priority, CacheItemRemovedCallback refreshAction, -// CacheDependency cacheDependency, TimeSpan timeout, GetCacheItemDelegate getCacheItem) -// { -// object result = System.Web.HttpRuntime.Cache.Get(cacheKey); -// if (result == null) -// { -// lock (syncLock) -// { -// result = System.Web.HttpRuntime.Cache.Get(cacheKey); -// if (result == null) -// { -// result = getCacheItem(); -// System.Web.HttpRuntime.Cache.Add(cacheKey, result, cacheDependency, -// DateTime.Now.Add(timeout), TimeSpan.Zero, priority, refreshAction); -// } -// } -// } -// return (TT)result; -// } - -// } -//} diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index f3daf20bb4..aef9f365ad 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -203,7 +203,6 @@ ASPXCodeBehind - From f1b398fab63cdf7399c6058f6284638ec4ee00c4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2020 11:06:21 +1100 Subject: [PATCH 03/74] Adds some null checks, need to see if tests pass --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 5 +++++ src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index f92d8adebb..e5b66e259c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -1028,8 +1028,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { parentLink = parentLink ?? GetRequiredParentLink(content, null); + // TODO: This can result in a null value? see https://github.com/umbraco/Umbraco-CMS/issues/7868 + // It seems to be related to having corrupt Paths in the umbracoNode table. var parent = parentLink.Value; + if (parent == null) + throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted, please see the HealthCheck dashboard and fixup data inconsistencies."); + // if parent has no children, clone parent + add as first child if (parent.FirstChildContentId < 0) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs index d187996df8..8794978852 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Web.PublishedCache.NuCache.Snap +using System; + +namespace Umbraco.Web.PublishedCache.NuCache.Snap { //NOTE: This cannot be struct because it references itself @@ -11,7 +13,7 @@ { public LinkedNode(TValue value, long gen, LinkedNode next = null) { - Value = value; + Value = value ?? throw new ArgumentNullException(nameof(value)); Gen = gen; Next = next; } From c9c869356001e55dcd30a3c3f7076ba0d01bb3b4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2020 13:13:15 +1100 Subject: [PATCH 04/74] removes one null check, adds notes --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 8 ++++++++ src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs | 6 ++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index e5b66e259c..9298201ecf 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -502,6 +502,14 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + /// + /// Validate the and try to create a parent + /// + /// + /// + /// + /// Returns false if the parent was not found or if the kit validation failed + /// private bool BuildKit(ContentNodeKit kit, out LinkedNode parent) { // make sure parent exists diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs index 8794978852..94f83ac4e5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs @@ -1,6 +1,4 @@ -using System; - -namespace Umbraco.Web.PublishedCache.NuCache.Snap +namespace Umbraco.Web.PublishedCache.NuCache.Snap { //NOTE: This cannot be struct because it references itself @@ -13,7 +11,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.Snap { public LinkedNode(TValue value, long gen, LinkedNode next = null) { - Value = value ?? throw new ArgumentNullException(nameof(value)); + Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked Gen = gen; Next = next; } From 3aa849fff2f91fbbde6e306e83a672634bb87806 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 1 Apr 2020 15:50:46 +0200 Subject: [PATCH 05/74] AB#5819 - Moved composing of core/infrastructure items into CoreInitialComposer --- .../Actions/ActionCollectionBuilder.cs | 2 +- src/Umbraco.Core/CompositionExtensions.cs | 6 +- .../ContentEditorContentAppFactory.cs | 2 +- .../ContentApps/ListViewContentAppFactory.cs | 2 +- .../DefaultEventMessagesFactory.cs | 2 +- .../HybridEventMessagesAccessor.cs | 2 +- src/Umbraco.Core/Services/SectionService.cs | 2 +- src/Umbraco.Core/Services/TreeService.cs | 2 +- .../CompositionExtensions.cs | 19 +- .../Runtime/CoreInitialComposer.cs | 185 +++++++++++++++++- .../Runtime/AspNetCoreComposer.cs | 29 +-- src/Umbraco.Web/CompositionExtensions.cs | 15 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 140 +------------ src/Umbraco.Web/Umbraco.Web.csproj | 1 - 14 files changed, 216 insertions(+), 193 deletions(-) rename src/{Umbraco.Web => Umbraco.Core}/DefaultEventMessagesFactory.cs (92%) diff --git a/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs b/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs index ec1a9210a7..eaea63b3a3 100644 --- a/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs +++ b/src/Umbraco.Core/Actions/ActionCollectionBuilder.cs @@ -4,7 +4,7 @@ using System.Linq; using Umbraco.Core.Composing; namespace Umbraco.Web.Actions { - internal class ActionCollectionBuilder : LazyCollectionBuilderBase + public class ActionCollectionBuilder : LazyCollectionBuilderBase { protected override ActionCollectionBuilder This => this; diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index a16abf76ac..bbea868f55 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core /// /// The composition. /// - internal static ActionCollectionBuilder Actions(this Composition composition) + public static ActionCollectionBuilder Actions(this Composition composition) => composition.WithCollectionBuilder(); /// @@ -45,7 +45,7 @@ namespace Umbraco.Core /// /// The composition. /// - internal static EditorValidatorCollectionBuilder EditorValidators(this Composition composition) + public static EditorValidatorCollectionBuilder EditorValidators(this Composition composition) => composition.WithCollectionBuilder(); /// @@ -81,7 +81,7 @@ namespace Umbraco.Core /// The composition. public static SectionCollectionBuilder Sections(this Composition composition) => composition.WithCollectionBuilder(); - + /// /// Gets the components collection builder. /// diff --git a/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs index add7e2f16a..467bc6c29f 100644 --- a/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ContentEditorContentAppFactory.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Web.ContentApps { - internal class ContentEditorContentAppFactory : IContentAppFactory + public class ContentEditorContentAppFactory : IContentAppFactory { // see note on ContentApp internal const int Weight = -100; diff --git a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs index 3e0dea0f5e..d66de3b238 100644 --- a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs @@ -10,7 +10,7 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.ContentApps { - internal class ListViewContentAppFactory : IContentAppFactory + public class ListViewContentAppFactory : IContentAppFactory { // see note on ContentApp private const int Weight = -666; diff --git a/src/Umbraco.Web/DefaultEventMessagesFactory.cs b/src/Umbraco.Core/DefaultEventMessagesFactory.cs similarity index 92% rename from src/Umbraco.Web/DefaultEventMessagesFactory.cs rename to src/Umbraco.Core/DefaultEventMessagesFactory.cs index 39be829a7d..0d53645b5e 100644 --- a/src/Umbraco.Web/DefaultEventMessagesFactory.cs +++ b/src/Umbraco.Core/DefaultEventMessagesFactory.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Events; namespace Umbraco.Web { - internal class DefaultEventMessagesFactory : IEventMessagesFactory + public class DefaultEventMessagesFactory : IEventMessagesFactory { private readonly IEventMessagesAccessor _eventMessagesAccessor; diff --git a/src/Umbraco.Core/HybridEventMessagesAccessor.cs b/src/Umbraco.Core/HybridEventMessagesAccessor.cs index b2700eb137..82e784e093 100644 --- a/src/Umbraco.Core/HybridEventMessagesAccessor.cs +++ b/src/Umbraco.Core/HybridEventMessagesAccessor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Events; namespace Umbraco.Web { - internal class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor + public class HybridEventMessagesAccessor : HybridAccessorBase, IEventMessagesAccessor { protected override string ItemKey => "Umbraco.Core.Events.HybridEventMessagesAccessor"; diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 7f97b7b71a..f66ab5ef62 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -7,7 +7,7 @@ using Umbraco.Web.Sections; namespace Umbraco.Web.Services { - internal class SectionService : ISectionService + public class SectionService : ISectionService { private readonly IUserService _userService; private readonly SectionCollection _sectionCollection; diff --git a/src/Umbraco.Core/Services/TreeService.cs b/src/Umbraco.Core/Services/TreeService.cs index dc500f2583..48cc98b2db 100644 --- a/src/Umbraco.Core/Services/TreeService.cs +++ b/src/Umbraco.Core/Services/TreeService.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.Services /// /// Implements . /// - internal class TreeService : ITreeService + public class TreeService : ITreeService { private readonly TreeCollection _treeCollection; diff --git a/src/Umbraco.Infrastructure/CompositionExtensions.cs b/src/Umbraco.Infrastructure/CompositionExtensions.cs index bd221e6fd3..d55effcd7a 100644 --- a/src/Umbraco.Infrastructure/CompositionExtensions.cs +++ b/src/Umbraco.Infrastructure/CompositionExtensions.cs @@ -11,6 +11,8 @@ using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Web.Media.EmbedProviders; +using Umbraco.Web.Search; namespace Umbraco.Core { @@ -84,7 +86,20 @@ namespace Umbraco.Core public static ManifestFilterCollectionBuilder ManifestFilters(this Composition composition) => composition.WithCollectionBuilder(); - + /// + /// Gets the backoffice OEmbed Providers collection builder. + /// + /// The composition. + public static EmbedProvidersCollectionBuilder OEmbedProviders(this Composition composition) + => composition.WithCollectionBuilder(); + + /// + /// Gets the back office searchable tree collection builder + /// + /// + /// + public static SearchableTreeCollectionBuilder SearchableTrees(this Composition composition) + => composition.WithCollectionBuilder(); #endregion @@ -98,7 +113,7 @@ namespace Umbraco.Core public static void SetCultureDictionaryFactory(this Composition composition) where T : ICultureDictionaryFactory { - composition.RegisterUnique(); + composition.RegisterUnique(); } /// diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index aa9ccb958f..3a3354b322 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -1,4 +1,5 @@ using System; +using Examine; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Composing.CompositionExtensions; @@ -7,9 +8,11 @@ using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dashboards; using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; +using Umbraco.Core.Media; using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Migrations.PostMigrations; @@ -17,19 +20,35 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.Validators; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Scoping; using Umbraco.Core.Serialization; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Examine; +using Umbraco.Infrastructure.Media; using Umbraco.Web; +using Umbraco.Web.Actions; +using Umbraco.Web.Cache; +using Umbraco.Web.ContentApps; +using Umbraco.Web.Editors; +using Umbraco.Web.Features; +using Umbraco.Web.HealthCheck; +using Umbraco.Web.HealthCheck.NotificationMethods; using Umbraco.Web.Install; +using Umbraco.Web.Macros; +using Umbraco.Web.Media.EmbedProviders; using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.PropertyEditors; using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Search; +using Umbraco.Web.Sections; using Umbraco.Web.Services; +using Umbraco.Web.Templates; using Umbraco.Web.Trees; using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; @@ -42,7 +61,7 @@ namespace Umbraco.Core.Runtime public override void Compose(Composition composition) { base.Compose(composition); - + // composers composition .ComposeRepositories() @@ -177,6 +196,170 @@ namespace Umbraco.Core.Runtime // Config manipulator composition.RegisterUnique(); + + + // register the http context and umbraco context accessors + // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when + // we have no http context, eg when booting Umbraco or in background threads, so instead + // let's use an hybrid accessor that can fall back to a ThreadStatic context. + composition.RegisterUnique(); + + // register the umbraco context factory + // composition.RegisterUnique(); + composition.RegisterUnique(); + + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + + // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be + // discovered when CoreBootManager configures the converters. We HAVE to remove one of them + // here because there cannot be two converters for one property editor - and we want the full + // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. + // (the limited one, defined in Core, is there for tests) - same for others + composition.PropertyValueConverters() + .Remove() + .Remove() + .Remove(); + + composition.UrlProviders() + .Append() + .Append(); + + composition.MediaUrlProviders() + .Append(); + + composition.RegisterUnique(); + + // register properties fallback + composition.RegisterUnique(); + + composition.RegisterUnique(); + + composition.RegisterUnique(); + + composition.Actions() + .Add(() => composition.TypeLoader.GetTypes()); + + composition.EditorValidators() + .Add(() => composition.TypeLoader.GetTypes()); + + + composition.TourFilters(); + + // replace with web implementation + composition.RegisterUnique(); + + // register OEmbed providers - no type scanning - all explicit opt-in of adding types + // note: IEmbedProvider is not IDiscoverable - think about it if going for type scanning + composition.OEmbedProviders() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + // register back office sections in the order we want them rendered + composition.Sections() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + // register known content apps + composition.ContentApps() + .Append() + .Append() + .Append(); + + // register published router + composition.RegisterUnique(); + + // register *all* checks, except those marked [HideFromTypeFinder] of course + composition.HealthChecks() + .Add(() => composition.TypeLoader.GetTypes()); + + + composition.WithCollectionBuilder() + .Add(() => composition.TypeLoader.GetTypes()); + + composition.RegisterUnique(); + + composition.ContentFinders() + // all built-in finders in the correct order, + // devs can then modify this list on application startup + .Append() + .Append() + .Append() + //.Append() // disabled, this is an odd finder + .Append() + .Append(); + + composition.Register(Lifetime.Request); + + composition.SearchableTrees() + .Add(() => composition.TypeLoader.GetTypes()); + + // replace some services + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + + composition.RegisterUnique(); + + // register distributed cache + composition.RegisterUnique(f => new DistributedCache(f.GetInstance(), f.GetInstance())); + + + composition.Register(Lifetime.Request); + + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + + // we should stop injecting UmbracoContext and always inject IUmbracoContextAccessor, however at the moment + // there are tons of places (controllers...) which require UmbracoContext in their ctor - so let's register + // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context + // in it's own way but we don't want that to happen, we manage its lifetime ourselves. + composition.Register(factory => factory.GetInstance().UmbracoContext); + composition.RegisterUnique(); + composition.Register(factory => + { + var umbCtx = factory.GetInstance(); + return new PublishedContentQuery(umbCtx.UmbracoContext.PublishedSnapshot, factory.GetInstance(), factory.GetInstance()); + }, Lifetime.Request); + + + composition.RegisterUnique(); + + // register the http context and umbraco context accessors + // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when + // we have no http context, eg when booting Umbraco or in background threads, so instead + // let's use an hybrid accessor that can fall back to a ThreadStatic context. + composition.RegisterUnique(); + + // register accessors for cultures + composition.RegisterUnique(); + + + + } } } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index e7c589bc17..4080c5a8fe 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -43,37 +43,12 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(factory => factory.GetInstance()); composition.RegisterUnique(factory => factory.GetInstance()); + + //Password hasher composition.RegisterUnique(); - // register the http context and umbraco context accessors - // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when - // we have no http context, eg when booting Umbraco or in background threads, so instead - // let's use an hybrid accessor that can fall back to a ThreadStatic context. - composition.RegisterUnique(); - // register the umbraco context factory - // composition.RegisterUnique(); - composition.RegisterUnique(); - - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - - composition.UrlProviders() - .Append() - .Append(); - - composition.MediaUrlProviders() - .Append(); - - composition.RegisterUnique(); - - // register properties fallback - composition.RegisterUnique(); - - composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Web/CompositionExtensions.cs b/src/Umbraco.Web/CompositionExtensions.cs index 35e60740dd..29a892f1d5 100644 --- a/src/Umbraco.Web/CompositionExtensions.cs +++ b/src/Umbraco.Web/CompositionExtensions.cs @@ -40,12 +40,7 @@ namespace Umbraco.Web => composition.WithCollectionBuilder(); - /// - /// Gets the backoffice OEmbed Providers collection builder. - /// - /// The composition. - public static EmbedProvidersCollectionBuilder OEmbedProviders(this Composition composition) - => composition.WithCollectionBuilder(); + /// /// Gets the back office tree collection builder @@ -55,14 +50,6 @@ namespace Umbraco.Web public static TreeCollectionBuilder Trees(this Composition composition) => composition.WithCollectionBuilder(); - /// - /// Gets the back office searchable tree collection builder - /// - /// - /// - public static SearchableTreeCollectionBuilder SearchableTrees(this Composition composition) - => composition.WithCollectionBuilder(); - #endregion diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 29baba8592..e3d21d2dd3 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -95,43 +95,14 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - // register accessors for cultures - composition.RegisterUnique(); - - - // register the http context and umbraco context accessors - // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when - // we have no http context, eg when booting Umbraco or in background threads, so instead - // let's use an hybrid accessor that can fall back to a ThreadStatic context. - composition.RegisterUnique(); - // register the umbraco context factory composition.RegisterUnique(); - composition.RegisterUnique(); - - // we should stop injecting UmbracoContext and always inject IUmbracoContextAccessor, however at the moment - // there are tons of places (controllers...) which require UmbracoContext in their ctor - so let's register - // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context - // in it's own way but we don't want that to happen, we manage its lifetime ourselves. - composition.Register(factory => factory.GetInstance().UmbracoContext); - composition.RegisterUnique(); - composition.Register(factory => - { - var umbCtx = factory.GetInstance(); - return new PublishedContentQuery(umbCtx.UmbracoContext.PublishedSnapshot, factory.GetInstance(), factory.GetInstance()); - }, Lifetime.Request); - composition.Register(Lifetime.Request); composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) // so inject a "void" helper (not exactly pretty but...) @@ -145,19 +116,8 @@ namespace Umbraco.Web.Runtime else composition.Register(_ => new UmbracoHelper()); - // register distributed cache - composition.RegisterUnique(f => new DistributedCache(f.GetInstance(), f.GetInstance())); - composition.RegisterUnique(); - // replace some services - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - - composition.RegisterUnique(); - // configure the container for web composition.ConfigureForWeb(); @@ -165,21 +125,6 @@ namespace Umbraco.Web.Runtime .ComposeUmbracoControllers(GetType().Assembly) .SetDefaultRenderMvcController(); // default controller for template views - composition.SearchableTrees() - .Add(() => composition.TypeLoader.GetTypes()); - - composition.Register(Lifetime.Request); - - composition.EditorValidators() - .Add(() => composition.TypeLoader.GetTypes()); - - composition.TourFilters(); - - composition.RegisterUnique(); - - composition.Actions() - .Add(() => composition.TypeLoader.GetTypes()); - //we need to eagerly scan controller types since they will need to be routed composition.WithCollectionBuilder() .Add(composition.TypeLoader.GetSurfaceControllers()); @@ -187,113 +132,32 @@ namespace Umbraco.Web.Runtime composition.WithCollectionBuilder() .Add(umbracoApiControllerTypes); - // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be - // discovered when CoreBootManager configures the converters. We HAVE to remove one of them - // here because there cannot be two converters for one property editor - and we want the full - // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. - // (the limited one, defined in Core, is there for tests) - same for others - composition.PropertyValueConverters() - .Remove() - .Remove() - .Remove(); // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax composition.FilteredControllerFactory() .Append(); - composition.UrlProviders() - .Append() - .Append(); - - composition.MediaUrlProviders() - .Append(); - - composition.RegisterUnique(); - - composition.RegisterUnique(); - - composition.ContentFinders() - // all built-in finders in the correct order, - // devs can then modify this list on application startup - .Append() - .Append() - .Append() - //.Append() // disabled, this is an odd finder - .Append() - .Append(); - - composition.RegisterUnique(); - - // register *all* checks, except those marked [HideFromTypeFinder] of course - composition.HealthChecks() - .Add(() => composition.TypeLoader.GetTypes()); - - composition.WithCollectionBuilder() - .Add(() => composition.TypeLoader.GetTypes()); - // auto-register views composition.RegisterAuto(typeof(UmbracoViewPage<>)); - // register published router - composition.RegisterUnique(); - // register preview SignalR hub composition.RegisterUnique(_ => GlobalHost.ConnectionManager.GetHubContext()); - // register properties fallback - composition.RegisterUnique(); - - // register known content apps - composition.ContentApps() - .Append() - .Append() - .Append(); - - // register back office sections in the order we want them rendered - composition.Sections() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - // register back office trees // the collection builder only accepts types inheriting from TreeControllerBase // and will filter out those that are not attributed with TreeAttribute composition.Trees() .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); - // register OEmbed providers - no type scanning - all explicit opt-in of adding types - // note: IEmbedProvider is not IDiscoverable - think about it if going for type scanning - composition.OEmbedProviders() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - - - // replace with web implementation - composition.RegisterUnique(); // Config manipulator composition.RegisterUnique(); //ApplicationShutdownRegistry composition.RegisterUnique(); + + } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 18097fd47f..902a72b092 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -323,7 +323,6 @@ - From 97cf099f9f517227f6a33bfc09a9300f10b90a07 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 1 Apr 2020 20:00:27 +0200 Subject: [PATCH 06/74] AB#5819 - Moved composing of core/infrastructure items into CoreInitialComposer --- .../CompositionExtensions_Uniques.cs | 17 ++++++- src/Umbraco.Core/Session/ISessionManager.cs | 4 +- .../AspNetCore/AspNetCoreCookieManager.cs | 46 ++++++++++++++++++ .../AspNetCore/AspNetCoreSessionIdResolver.cs | 30 ------------ .../AspNetCore/AspNetCoreSessionManager.cs | 48 +++++++++++++++++++ .../UmbracoCoreServiceCollectionExtensions.cs | 2 +- .../Runtime/AspNetCoreComposer.cs | 11 +++-- .../AspNet/AspNetSessionManager.cs | 8 ++-- .../WebMappingProfiles.cs | 2 +- src/Umbraco.Web/Macros/MacroRenderer.cs | 2 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 40 ++++------------ 11 files changed, 138 insertions(+), 72 deletions(-) create mode 100644 src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs delete mode 100644 src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs create mode 100644 src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs diff --git a/src/Umbraco.Core/CompositionExtensions_Uniques.cs b/src/Umbraco.Core/CompositionExtensions_Uniques.cs index 52f1ce7ecd..8352eb33ec 100644 --- a/src/Umbraco.Core/CompositionExtensions_Uniques.cs +++ b/src/Umbraco.Core/CompositionExtensions_Uniques.cs @@ -31,5 +31,20 @@ namespace Umbraco.Core /// public static void RegisterUnique(this Composition composition, TService instance) => composition.RegisterUnique(typeof(TService), instance); + + + + /// + /// Registers a unique service with an implementation type. + /// + public static void RegisterMultipleUnique(this Composition composition) + where TImplementing : class, TService1, TService2 + where TService1 : class + where TService2 : class + { + composition.RegisterUnique(); + composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterUnique(factory => factory.GetInstance()); + } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Session/ISessionManager.cs b/src/Umbraco.Core/Session/ISessionManager.cs index f3a47202ee..642122f2d3 100644 --- a/src/Umbraco.Core/Session/ISessionManager.cs +++ b/src/Umbraco.Core/Session/ISessionManager.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Session { public interface ISessionManager { - object GetSessionValue(string sessionName); - void SetSessionValue(string sessionName, object value); + string GetSessionValue(string sessionName); + void SetSessionValue(string sessionName, string value); } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs new file mode 100644 index 0000000000..79190a6776 --- /dev/null +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.AspNetCore.Http; +using Umbraco.Core.Cookie; + +namespace Umbraco.Web.Common.AspNetCore +{ + public class AspNetCoreCookieManager : ICookieManager + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public AspNetCoreCookieManager(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public void ExpireCookie(string cookieName) + { + var httpContext = _httpContextAccessor.HttpContext; + + if (httpContext is null) return; + + var cookieValue = httpContext.Request.Cookies[cookieName]; + + httpContext.Response.Cookies.Append(cookieName, cookieValue, new CookieOptions() + { + Expires = DateTime.Now.AddYears(-1) + }); + } + + public string GetCookieValue(string cookieName) + { + return _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; + } + + public void SetCookieValue(string cookieName, string value) + { + _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value); + } + + public bool HasCookie(string cookieName) + { + return !(GetCookieValue(cookieName) is null); + } + + } +} diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs deleted file mode 100644 index 818a39fac5..0000000000 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Umbraco.Net; - -namespace Umbraco.Web.Common.AspNetCore -{ - internal class AspNetCoreSessionIdResolver : ISessionIdResolver - { - private readonly IHttpContextAccessor _httpContextAccessor; - - public AspNetCoreSessionIdResolver(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - - public string SessionId - { - get - { - var httpContext = _httpContextAccessor?.HttpContext; - // If session isn't enabled this will throw an exception so we check - var sessionFeature = httpContext?.Features.Get(); - return sessionFeature != null - ? httpContext?.Session?.Id - : "0"; - } - } - } -} diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs new file mode 100644 index 0000000000..08b4a46aef --- /dev/null +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionManager.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Umbraco.Core.Session; +using Umbraco.Net; + +namespace Umbraco.Web.Common.AspNetCore +{ + internal class AspNetCoreSessionManager : ISessionIdResolver, ISessionManager + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public AspNetCoreSessionManager(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + + /// + /// If session isn't enabled this will throw an exception so we check + /// + private bool IsSessionsAvailable => !(_httpContextAccessor.HttpContext?.Features.Get() is null); + + public string SessionId + { + get + { + var httpContext = _httpContextAccessor?.HttpContext; + + return IsSessionsAvailable + ? httpContext?.Session?.Id + : "0"; + } + } + + public string GetSessionValue(string sessionName) + { + if(!IsSessionsAvailable) return null; + return _httpContextAccessor.HttpContext?.Session.GetString(sessionName); + } + + + public void SetSessionValue(string sessionName, string value) + { + if(!IsSessionsAvailable) return; + _httpContextAccessor.HttpContext?.Session.SetString(sessionName, value); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index d8f8fc1aed..916e886f50 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -164,7 +164,7 @@ namespace Umbraco.Web.Common.Extensions hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment, httpContextAccessor); ioHelper = new IOHelper(hostingEnvironment, globalSettings); logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, - new AspNetCoreSessionIdResolver(httpContextAccessor), + new AspNetCoreSessionManager(httpContextAccessor), // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured () => services.BuildServiceProvider().GetService(), coreDebug, ioHelper, new AspNetCoreMarchal()); diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 4080c5a8fe..fb70790143 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -1,12 +1,14 @@ using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Cookie; using Umbraco.Core.Hosting; using Umbraco.Core.Media; using Umbraco.Core.Models.PublishedContent; using Umbraco.Net; using Umbraco.Core.Runtime; using Umbraco.Core.Security; +using Umbraco.Core.Session; using Umbraco.Infrastructure.Media; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Lifetime; @@ -39,15 +41,18 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); // The umbraco request lifetime - composition.RegisterUnique(); - composition.RegisterUnique(factory => factory.GetInstance()); - composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterMultipleUnique(); //Password hasher composition.RegisterUnique(); + composition.RegisterUnique(); + + composition.RegisterMultipleUnique(); + + composition.RegisterUnique(); } } diff --git a/src/Umbraco.Web/AspNet/AspNetSessionManager.cs b/src/Umbraco.Web/AspNet/AspNetSessionManager.cs index f81daadb0a..d975dac528 100644 --- a/src/Umbraco.Web/AspNet/AspNetSessionManager.cs +++ b/src/Umbraco.Web/AspNet/AspNetSessionManager.cs @@ -11,13 +11,15 @@ namespace Umbraco.Web.AspNet { } - public object GetSessionValue(string sessionName) + public string GetSessionValue(string sessionName) { - return HttpContext.Current.Session[sessionName]; + return HttpContext.Current?.Session[sessionName]?.ToString(); } - public void SetSessionValue(string sessionName, object value) + public void SetSessionValue(string sessionName, string value) { + var httpContext = HttpContext.Current; + if (httpContext is null) return; HttpContext.Current.Session[sessionName] = value; } diff --git a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs index d5f3ba244b..21a242ee17 100644 --- a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs +++ b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.Composing.CompositionExtensions .Add() .Add() .Add() - .Add();; + .Add(); composition.Register(); composition.Register(); diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs index 624395049e..b79922474b 100644 --- a/src/Umbraco.Web/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web/Macros/MacroRenderer.cs @@ -403,7 +403,7 @@ namespace Umbraco.Web.Macros attributeValue = _requestAccessor.GetRequestValue(name); break; case '%': - attributeValue = _sessionManager.GetSessionValue(name)?.ToString(); + attributeValue = _sessionManager.GetSessionValue(name); if (string.IsNullOrEmpty(attributeValue)) attributeValue = _cookieManager.GetCookieValue(name); break; diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index e3d21d2dd3..45fc6b2b69 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -3,53 +3,31 @@ using System.Web.Security; using Examine; using Microsoft.AspNet.SignalR; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Cookie; using Umbraco.Core.Dictionary; -using Umbraco.Core.Events; using Umbraco.Core.Hosting; using Umbraco.Core.Install; -using Umbraco.Core.Migrations.PostMigrations; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Runtime; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Core.Sync; -using Umbraco.Web.Actions; -using Umbraco.Web.Cache; using Umbraco.Web.Composing.CompositionExtensions; -using Umbraco.Web.ContentApps; -using Umbraco.Web.Editors; -using Umbraco.Web.Features; -using Umbraco.Web.HealthCheck; using Umbraco.Web.Hosting; using Umbraco.Web.Install; using Umbraco.Web.Macros; -using Umbraco.Web.Media.EmbedProviders; -using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Search; -using Umbraco.Web.Sections; using Umbraco.Web.Security; using Umbraco.Web.Security.Providers; -using Umbraco.Web.Services; using Umbraco.Web.SignalR; using Umbraco.Web.Templates; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; -using Umbraco.Web.PropertyEditors; -using Umbraco.Examine; using Umbraco.Net; using Umbraco.Core.Request; using Umbraco.Core.Session; using Umbraco.Web.AspNet; -using Umbraco.Core.Media; -using Umbraco.Infrastructure.Media; namespace Umbraco.Web.Runtime { @@ -66,9 +44,7 @@ namespace Umbraco.Web.Runtime composition.Register(); composition.Register(); - composition.Register(Lifetime.Singleton); - composition.Register(factory => factory.GetInstance(), Lifetime.Singleton); - composition.Register(factory => factory.GetInstance(), Lifetime.Singleton); + composition.Register(Lifetime.Singleton); @@ -76,8 +52,7 @@ namespace Umbraco.Web.Runtime composition.Register(); composition.Register(Lifetime.Singleton); - composition.RegisterUnique(); // required for hybrid accessors - composition.RegisterUnique(); + composition.ComposeWebMappingProfiles(); @@ -151,12 +126,17 @@ namespace Umbraco.Web.Runtime .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); - // Config manipulator - composition.RegisterUnique(); - //ApplicationShutdownRegistry + // STUFF that do not have to be moved to .NET CORE + //---------------------------------------- + composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); // required for hybrid accessors + composition.Register(Lifetime.Singleton); + composition.Register(factory => factory.GetInstance(), Lifetime.Singleton); + composition.Register(factory => factory.GetInstance(), Lifetime.Singleton); } } From 50e9f79ae6f878e837fc0d4748a803378c8d0216 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Apr 2020 23:10:52 +1000 Subject: [PATCH 07/74] Adds a null check if things are out of order when building up the tree/linked list --- .../PublishedCache/NuCache/ContentStore.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 9298201ecf..474c390027 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -520,6 +520,15 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; } + // We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path + // data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null + // because the data sort operation is by path. + if (parent.Value == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}."); + return false; + } + // make sure the kit is valid if (kit.DraftData == null && kit.PublishedData == null) { @@ -1036,12 +1045,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { parentLink = parentLink ?? GetRequiredParentLink(content, null); - // TODO: This can result in a null value? see https://github.com/umbraco/Umbraco-CMS/issues/7868 - // It seems to be related to having corrupt Paths in the umbracoNode table. var parent = parentLink.Value; + // We are doing a null check here but this should no longer be possible because we have a null check in BuildKit + // for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place. + // see https://github.com/umbraco/Umbraco-CMS/issues/7868 if (parent == null) - throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted, please see the HealthCheck dashboard and fixup data inconsistencies."); + throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted."); // if parent has no children, clone parent + add as first child if (parent.FirstChildContentId < 0) From 4b467bf4702b2662cccf478cdb61b60eea17d70b Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Apr 2020 01:02:08 +1000 Subject: [PATCH 08/74] Creates data integrity health checks --- .../Repositories/IContentRepository.cs | 7 + .../Implement/ContentRepositoryBase.cs | 165 +++++++++++++++++- src/Umbraco.Core/Services/IContentService.cs | 2 +- .../Services/IContentServiceBase.cs | 13 +- .../Services/Implement/ContentService.cs | 20 +++ .../Services/Implement/MediaService.cs | 24 +++ .../Checks/Data/DatabaseIntegrityCheck.cs | 97 ++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 8 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index 217719e144..aff7a58652 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -77,5 +77,12 @@ namespace Umbraco.Core.Persistence.Repositories /// Here, can be null but cannot. IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); + + /// + /// Checks the data integrity of the node paths stored in the database + /// + bool VerifyNodePaths(out int[] invalidIds); + + void FixNodePaths(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 13b687eb4e..cd79923323 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -403,7 +403,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // content type alias is invariant - if(ordering.OrderBy.InvariantEquals("contentTypeAlias")) + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) { var joins = Sql() .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); @@ -477,6 +477,169 @@ namespace Umbraco.Core.Persistence.Repositories.Implement IQuery filter, Ordering ordering); + public bool VerifyNodePaths(out int[] invalidIds) + { + var invalid = new List(); + + var sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // TODO: Could verify sort orders here too + + var currentParentIds = new HashSet { -1 }; + var prevParentIds = currentParentIds; + var lastLevel = -1; + + // use a forward cursor (query) + foreach (var node in Database.Query(sql)) + { + if (node.Level != lastLevel) + { + // changing levels + prevParentIds = currentParentIds; + currentParentIds = null; + lastLevel = node.Level; + } + + if (currentParentIds == null) + { + // we're reset + currentParentIds = new HashSet(); + } + + currentParentIds.Add(node.NodeId); + + var pathParts = node.Path.Split(','); + + if (!prevParentIds.Contains(node.ParentId)) + { + // invalid, this will be because the level is wrong + invalid.Add(node.NodeId); + } + else if (pathParts.Length < 2) + { + // invalid path + invalid.Add(node.NodeId); + } + else if (pathParts.Length - 1 != node.Level) + { + // invalid, either path or level is wrong + invalid.Add(node.NodeId); + } + else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) + { + // invalid path + invalid.Add(node.NodeId); + } + else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString()) + { + // invalid path + invalid.Add(node.NodeId); + } + } + + invalidIds = invalid.ToArray(); + return invalid.Count == 0; + } + + public void FixNodePaths() + { + // TODO: We can probably combine this logic with the above + + var invalid = new List<(int child, int parent)>(); + + var sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // TODO: Could verify sort orders here too + + var updated = new List(); + var missingParentIds = new Dictionary>(); + var currentParentIds = new HashSet { -1 }; + var prevParentIds = currentParentIds; + var lastLevel = -1; + + // use a forward cursor (query) + foreach (var node in Database.Query(sql)) + { + if (node.Level != lastLevel) + { + // changing levels + prevParentIds = currentParentIds; + currentParentIds = null; + lastLevel = node.Level; + } + + if (currentParentIds == null) + { + // we're reset + currentParentIds = new HashSet(); + } + + currentParentIds.Add(node.NodeId); + + var pathParts = node.Path.Split(','); + + if (!prevParentIds.Contains(node.ParentId)) + { + // invalid, this will be because the level is wrong (which prob means path is wrong too) + invalid.Add((node.NodeId, node.ParentId)); + if (missingParentIds.TryGetValue(node.ParentId, out var childIds)) + childIds.Add(node); + else + missingParentIds[node.ParentId] = new List {node}; + } + else if (pathParts.Length < 2) + { + // invalid path + invalid.Add((node.NodeId, node.ParentId)); + } + else if (pathParts.Length - 1 != node.Level) + { + // invalid, either path or level is wrong + invalid.Add((node.NodeId, node.ParentId)); + } + else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) + { + // invalid path + invalid.Add((node.NodeId, node.ParentId)); + } + else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString()) + { + // invalid path + invalid.Add((node.NodeId, node.ParentId)); + } + else + { + // it's valid + + if (missingParentIds.TryGetValue(node.NodeId, out var invalidNodes)) + { + // this parent has been flagged as missing which means one or more of it's children was ordered + // wrong and was checked first. So now we can try to rebuild the invalid paths. + + foreach (var invalidNode in invalidNodes) + { + invalidNode.Level = (short) (node.Level + 1); + invalidNode.Path = node.Path + "," + invalidNode.NodeId; + updated.Add(invalidNode); + } + } + } + } + + foreach (var node in updated) + { + Database.Update(node); + } + } + // here, filter can be null and ordering cannot protected IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6f9ca58821..58279fb4da 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -526,6 +526,6 @@ namespace Umbraco.Core.Services OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId); #endregion - + } } diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs index 439c55d0d0..1c04e0b4a3 100644 --- a/src/Umbraco.Core/Services/IContentServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentServiceBase.cs @@ -5,5 +5,16 @@ /// TODO: Start sharing the logic! /// public interface IContentServiceBase : IService - { } + { + + /// + /// Checks the data integrity of the node paths/levels stored in the database + /// + bool VerifyNodePaths(out int[] invalidIds); + + /// + /// Fixes the data integrity of node paths/levels stored in the database + /// + void FixNodePaths(); + } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1558b0170b..5d010d321f 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2375,6 +2375,26 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Succeed(evtMsgs); } + public bool VerifyNodePaths(out int[] invalidIds) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.VerifyNodePaths(out invalidIds); + } + } + + public void FixNodePaths() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.ContentTree); + _documentRepository.FixNodePaths(); + + // TODO: We're going to have to clear all caches + } + } + #endregion #region Internal Methods diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 528d0a0bf9..94f57bd859 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1139,6 +1139,28 @@ namespace Umbraco.Core.Services.Implement } return true; + + } + + + public bool VerifyNodePaths(out int[] invalidIds) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MediaTree); + return _mediaRepository.VerifyNodePaths(out invalidIds); + } + } + + public void FixNodePaths() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.MediaTree); + _mediaRepository.FixNodePaths(); + + // TODO: We're going to have to clear all caches + } } #endregion @@ -1358,5 +1380,7 @@ namespace Umbraco.Core.Services.Implement } #endregion + + } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs new file mode 100644 index 0000000000..d7bf62067f --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Services; + +namespace Umbraco.Web.HealthCheck.Checks.Data +{ + [HealthCheck( + "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", + "Database integrity check", + Description = "Checks for various data integrity issues in the Umbraco database.", + Group = "Data Integrity")] + public class DatabaseIntegrityCheck : HealthCheck + { + private readonly IContentService _contentService; + private readonly IMediaService _mediaService; + private const string _fixMediaPaths = "fixMediaPaths"; + private const string _fixContentPaths = "fixContentPaths"; + + public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService) + { + _contentService = contentService; + _mediaService = mediaService; + } + + /// + /// Get the status for this health check + /// + /// + public override IEnumerable GetStatus() + { + //return the statuses + return new[] + { + CheckContent(), + CheckMedia() + }; + } + + private HealthCheckStatus CheckMedia() + { + return CheckPaths(_fixMediaPaths, "Fix media paths", "media", () => + { + var mediaPaths = _mediaService.VerifyNodePaths(out var invalidMediaPaths); + return (mediaPaths, invalidMediaPaths); + }); + } + + private HealthCheckStatus CheckContent() + { + return CheckPaths(_fixContentPaths, "Fix content paths", "content", () => + { + var contentPaths = _contentService.VerifyNodePaths(out var invalidContentPaths); + return (contentPaths, invalidContentPaths); + }); + } + + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, Func<(bool success, int[] invalidPaths)> doCheck) + { + var result = doCheck(); + + var actions = new List(); + if (!result.success) + { + actions.Add(new HealthCheckAction(actionAlias, Id) + { + Name = actionName + }); + } + + return new HealthCheckStatus(result.success + ? $"All {entityType} paths are valid" + : $"There are {result.invalidPaths.Length} invalid {entityType} paths") + { + ResultType = result.success ? StatusResultType.Success : StatusResultType.Error, + Actions = actions + }; + } + + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) + { + case _fixContentPaths: + _contentService.FixNodePaths(); + return CheckContent(); + case _fixMediaPaths: + _mediaService.FixNodePaths(); + return CheckMedia(); + default: + throw new InvalidOperationException("Action not supported"); + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c3024f63ae..e39687bed8 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -156,6 +156,7 @@ + From 727578e51c57b202cad93d04858b64bf98b3893c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Apr 2020 13:29:00 +1000 Subject: [PATCH 09/74] Fixes media moving and massively improves moving performance --- src/Umbraco.Core/Constants-SqlTemplates.cs | 17 +++++ .../Implement/ContentRepositoryBase.cs | 14 ++-- .../Repositories/Implement/MediaRepository.cs | 72 +++++++++++-------- .../Services/Implement/MediaService.cs | 43 ++++++----- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 5 files changed, 92 insertions(+), 55 deletions(-) create mode 100644 src/Umbraco.Core/Constants-SqlTemplates.cs diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs new file mode 100644 index 0000000000..33c94f0f93 --- /dev/null +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class SqlTemplates + { + public const string VersionableRepositoryGetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; + public const string VersionableRepositoryGetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; + public const string VersionableRepositoryGetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; + public const string VersionableRepositoryEnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; + public const string VersionableRepositoryGetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; + public const string VersionableRepositoryGetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; + public const string VersionableRepositoryGetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; + + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index cd79923323..cf67244fbe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -83,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersionIds", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetVersionIds, tsql => tsql.Select(x => x.Id) .From() .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) @@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the version we want to delete - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersion", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetVersion, tsql => tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) ); var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); @@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersions", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetVersions, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) ); var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); @@ -933,7 +933,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryEnsureUniqueNodeName, tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); @@ -946,7 +946,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetNewChildSortOrder(int parentId, int first) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetSortOrder", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetSortOrder, tsql => tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From().Where(x => x.ParentId == SqlTemplate.Arg("parentId") && x.NodeObjectType == NodeObjectTypeId) ); @@ -955,7 +955,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual NodeDto GetParentNodeDto(int parentId) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetParentNode", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetParentNode, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("parentId")) ); @@ -964,7 +964,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetReservedId(Guid uniqueId) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetReservedId", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetReservedId, tsql => tsql.Select(x => x.NodeId).From().Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation) ); var id = Database.ExecuteScalar(template.Sql(new { uniqueId = uniqueId })); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 081efcfdf6..8518da8d8c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -219,7 +219,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMedia entity) { - var media = (Models.Media) entity; + var media = entity; entity.AddingEntity(); // ensure unique name on the same level @@ -298,26 +298,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMedia entity) { - var media = (Models.Media) entity; + var media = entity; // update media.UpdatingEntity(); - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below + // operations which will make this whole operation go much faster. When moving we don't need to create + // new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.GetDirtyProperties().All(x => x == nameof(entity.Path) || x == nameof(entity.Level) || x == nameof(entity.UpdateDate)); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) + if (!isMoving) { - var parent = GetParentNodeDto(entity.ParentId); + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty(nameof(entity.ParentId))) + { + var parent = GetParentNodeDto(entity.ParentId); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } } // create the dto @@ -328,26 +337,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement nodeDto.ValidatePathWithException(); Database.Update(nodeDto); - // update the content dto - Database.Update(dto.ContentDto); + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - // update the content & media version dtos - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - var mediaVersionDto = dto.MediaVersionDto; - contentVersionDto.Current = true; - Database.Update(contentVersionDto); - Database.Update(mediaVersionDto); + // update the content & media version dtos + var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + var mediaVersionDto = dto.MediaVersionDto; + contentVersionDto.Current = true; + Database.Update(contentVersionDto); + Database.Update(mediaVersionDto); - // replace the property data - var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); - Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + // replace the property data + var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); + Database.Execute(deletePropertyDataSql); + var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + foreach (var propertyDataDto in propertyDataDtos) + Database.Insert(propertyDataDto); - SetEntityTags(entity, _tagRepository); + SetEntityTags(entity, _tagRepository); - PersistRelations(entity); + PersistRelations(entity); + } OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 94f57bd859..aa3e07fa81 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -530,23 +530,27 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } - return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering); } } - private IEnumerable GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren, + private IQuery GetPagedDescendantQuery(string mediaPath) + { + var query = Query(); + if (!mediaPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); + return query; + } + + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var query = Query(); - if (!mediaPath.IsNullOrWhiteSpace()) - query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } @@ -888,7 +892,7 @@ namespace Umbraco.Core.Services.Implement public Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); + var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -940,7 +944,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Attempt.Succeed(evtMsgs); } - var moves = new List>(); + var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -979,7 +983,7 @@ namespace Umbraco.Core.Services.Implement // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything - private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection> moves, bool? trash) + private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash) { media.ParentId = parentId; @@ -989,7 +993,7 @@ namespace Umbraco.Core.Services.Implement var paths = new Dictionary(); - moves.Add(Tuple.Create(media, media.Path)); // capture original path + moves.Add((media, media.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = media.Path; @@ -1006,21 +1010,24 @@ namespace Umbraco.Core.Services.Implement paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id; const int pageSize = 500; - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) + var query = GetPagedDescendantQuery(originalPath); + long total; + do { - var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveMediaLocked(descendant, userId, trash); } - } + + } while (total > pageSize); + } private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash) @@ -1299,7 +1306,7 @@ namespace Umbraco.Core.Services.Implement // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); - var moves = new List>(); + var moves = new List<(IMedia, string)>(); var mediaTypeIdsA = mediaTypeIds.ToArray(); using (var scope = ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 24954e316c..efb76f2756 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,7 @@ --> + From ad698f9c19c6442dfbc66c80e3bbe299c235cf4c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Apr 2020 15:02:08 +1000 Subject: [PATCH 10/74] Fixes content service/repository for moving things correctly along with the perf improvements that were done to media. --- src/Umbraco.Core/ContentExtensions.cs | 10 + .../Implement/DocumentRepository.cs | 301 +++++++++--------- .../Repositories/Implement/MediaRepository.cs | 7 +- .../Services/Implement/ContentService.cs | 42 ++- .../Services/Implement/MediaService.cs | 1 + 5 files changed, 196 insertions(+), 165 deletions(-) diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 3edad0c963..3ee2a75b09 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -60,6 +60,16 @@ namespace Umbraco.Core #endregion + internal static bool IsMoving(this IContentBase entity) + { + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below + // operations which will make this whole operation go much faster. When moving we don't need to create + // new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.GetDirtyProperties().All(x => x == nameof(entity.Path) || x == nameof(entity.Level) || x == nameof(entity.UpdateDate)); + return isMoving; + } + /// /// Removes characters that are not valid XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index ccfa8209fb..a75165a7b9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -321,7 +321,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin() .On((c, d) => c.Id == d.Id) .Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - .Where( x => !x.Published) + .Where(x => !x.Published) ); var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); foreach (var versionDto in versionDtos) @@ -519,8 +519,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { - var entityBase = entity as EntityBase; - var isEntityDirty = entityBase != null && entityBase.IsDirty(); + var isEntityDirty = entity.IsDirty(); // check if we need to make any database changes at all if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) @@ -535,29 +534,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // update entity.UpdatingEntity(); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + var publishing = entity.PublishedState == PublishedState.Publishing; - // check if we need to create a new version - if (publishing && entity.PublishedVersionId > 0) + if (!isMoving) { - // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); - } + // check if we need to create a new version + if (publishing && entity.PublishedVersionId > 0) + { + // published version is not published anymore + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); + } - // sanitize names - SanitizeNames(entity, publishing); + // sanitize names + SanitizeNames(entity, publishing); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - var parent = GetParentNodeDto(entity.ParentId); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + var parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } } // create the dto @@ -568,146 +575,152 @@ namespace Umbraco.Core.Persistence.Repositories.Implement nodeDto.ValidatePathWithException(); Database.Update(nodeDto); - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & document version dtos - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - var documentVersionDto = dto.DocumentVersionDto; - if (publishing) + if (!isMoving) { - documentVersionDto.Published = true; // now published - contentVersionDto.Current = false; // no more current - } - Database.Update(contentVersionDto); - Database.Update(documentVersionDto); + // update the content dto + Database.Update(dto.ContentDto); - // and, if publishing, insert new content & document version dtos - if (publishing) - { - entity.PublishedVersionId = entity.VersionId; - - contentVersionDto.Id = 0; // want a new id - contentVersionDto.Current = true; // current version - contentVersionDto.Text = entity.Name; - Database.Insert(contentVersionDto); - entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - - documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); - } - - // replace the property data (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deletePropertyDataSql); - - // insert property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, - entity.Properties, LanguageRepository, out var edited, out var editedCultures); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); - - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && entity.PublishName != entity.Name) - edited = true; - - if (entity.ContentType.VariesByCulture()) - { - // bump dates to align cultures to version + // update the content & document version dtos + var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + var documentVersionDto = dto.DocumentVersionDto; if (publishing) - entity.AdjustDates(contentVersionDto.VersionDate); + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } + Database.Update(contentVersionDto); + Database.Update(documentVersionDto); - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos) - if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - { - edited = true; - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + // and, if publishing, insert new content & document version dtos + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; - // TODO: change tracking - // at the moment, we don't do any dirty tracking on property values, so we don't know whether the - // culture has just been edited or not, so we don't update its update date - that date only changes - // when the name is set, and it all works because the controller does it - but, if someone uses a - // service to change a property value and save (without setting name), the update date does not change. - } + contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version + contentVersionDto.Text = entity.Name; + Database.Insert(contentVersionDto); + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - // replace the content version variations (rather than updating) + documentVersionDto.Published = false; // non-published version + Database.Insert(documentVersionDto); + } + + // replace the property data (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet - var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deleteContentVariations); + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deletePropertyDataSql); - // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); - Database.Execute(deleteDocumentVariations); + // insert property data + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, + entity.Properties, LanguageRepository, out var edited, out var editedCultures); + foreach (var propertyDataDto in propertyDataDtos) + Database.Insert(propertyDataDto); - // TODO: NPoco InsertBulk issue? - // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) - // but by using SQL Server and updating a variants name will cause: Unable to cast object of type - // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. - // (same in PersistNewItem above) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) + edited = true; - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + if (entity.ContentType.VariesByCulture()) + { + // bump dates to align cultures to version + if (publishing) + entity.AdjustDates(contentVersionDto.VersionDate); - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in entity.CultureInfos) + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + edited = true; + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + + // TODO: change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. + } + + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); + Database.Execute(deleteDocumentVariations); + + // TODO: NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // (same in PersistNewItem above) + + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); + } + + // refresh content + entity.SetCultureEdited(editedCultures); + + // update the document dto + // at that point, when un/publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + dto.Published = true; + else if (entity.PublishedState == PublishedState.Unpublishing) + dto.Published = false; + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Update(dto); + + //update the schedule + if (entity.IsPropertyDirty("ContentSchedule")) + PersistContentSchedule(entity, true); + + // if entity is publishing, update tags, else leave tags there + // means that implicitly unpublished, or trashed, entities *still* have tags in db + if (entity.PublishedState == PublishedState.Publishing) + SetEntityTags(entity, _tagRepository); } - // refresh content - entity.SetCultureEdited(editedCultures); - - // update the document dto - // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) - dto.Published = true; - else if (entity.PublishedState == PublishedState.Unpublishing) - dto.Published = false; - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Update(dto); - - //update the schedule - if (entity.IsPropertyDirty("ContentSchedule")) - PersistContentSchedule(entity, true); - - // if entity is publishing, update tags, else leave tags there - // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (entity.PublishedState == PublishedState.Publishing) - SetEntityTags(entity, _tagRepository); - // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); - // flip the entity's published property - // this also flips its published state - if (entity.PublishedState == PublishedState.Publishing) + if (!isMoving) { - entity.Published = true; - entity.PublishTemplateId = entity.TemplateId; - entity.PublisherId = entity.WriterId; - entity.PublishName = entity.Name; - entity.PublishDate = entity.UpdateDate; + // flip the entity's published property + // this also flips its published state + if (entity.PublishedState == PublishedState.Publishing) + { + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; - SetEntityTags(entity, _tagRepository); + SetEntityTags(entity, _tagRepository); + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; + + ClearEntityTags(entity, _tagRepository); + } + + PersistRelations(entity); + + // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - entity.Published = false; - entity.PublishTemplateId = null; - entity.PublisherId = null; - entity.PublishName = null; - entity.PublishDate = null; - - ClearEntityTags(entity, _tagRepository); - } - - PersistRelations(entity); - - // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? entity.ResetDirtyProperties(); @@ -1183,7 +1196,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value)) temp.Content.PublishTemplateId = temp.Template2Id; } - + // set properties if (loadProperties) @@ -1216,7 +1229,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetVariations(temp.Content, contentVariations, documentVariations); } } - + foreach (var c in content) @@ -1430,7 +1443,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement yield return dto; } - + } private class ContentVariation diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 8518da8d8c..b9ef2d4086 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -304,10 +304,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement media.UpdatingEntity(); // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below - // operations which will make this whole operation go much faster. When moving we don't need to create - // new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.GetDirtyProperties().All(x => x == nameof(entity.Path) || x == nameof(entity.Level) || x == nameof(entity.UpdateDate)); + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); if (!isMoving) { diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 5d010d321f..f5f3d03abb 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -601,23 +601,27 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } - return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); } } - private IEnumerable GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren, + private IQuery GetPagedDescendantQuery(string contentPath) + { + var query = Query(); + if (!contentPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); + return query; + } + + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var query = Query(); - if (!contentPath.IsNullOrWhiteSpace()) - query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } @@ -1866,7 +1870,7 @@ namespace Umbraco.Core.Services.Implement public OperationResult MoveToRecycleBin(IContent content, int userId) { var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); + var moves = new List<(IContent, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -1925,7 +1929,7 @@ namespace Umbraco.Core.Services.Implement return; } - var moves = new List>(); + var moves = new List<(IContent, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -1978,7 +1982,7 @@ namespace Umbraco.Core.Services.Implement // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId, - ICollection> moves, + ICollection<(IContent, string)> moves, bool? trash) { content.WriterId = userId; @@ -1990,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement var paths = new Dictionary(); - moves.Add(Tuple.Create(content, content.Path)); // capture original path + moves.Add((content, content.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = content.Path; @@ -2007,20 +2011,24 @@ namespace Umbraco.Core.Services.Implement paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id; const int pageSize = 500; - var total = long.MaxValue; - while (total > 0) + var query = GetPagedDescendantQuery(originalPath); + long total; + do { - var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced + var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveContentLocked(descendant, userId, trash); } - } + + } while (total > pageSize); } @@ -2832,7 +2840,7 @@ namespace Umbraco.Core.Services.Implement // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); - var moves = new List>(); + var moves = new List<(IContent, string)>(); var contentTypeIdsA = contentTypeIds.ToArray(); // using an immediate uow here because we keep making changes with diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index aa3e07fa81..219beb7088 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1014,6 +1014,7 @@ namespace Umbraco.Core.Services.Implement long total; do { + // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); foreach (var descendant in descendants) From 18d9ad3c7367550fb2d6b9d49995c86330d8b9d4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Apr 2020 16:42:21 +1000 Subject: [PATCH 11/74] udpates health check and service methods to be flexible for data integrity checks, fixes tests --- src/Umbraco.Core/ContentExtensions.cs | 5 +- .../Models/ContentDataIntegrityReport.cs | 44 +++++ .../Models/ContentDataIntegrityReportEntry.cs | 13 ++ .../ContentDataIntegrityReportOptions.cs | 13 ++ .../Repositories/IContentRepository.cs | 8 +- .../Implement/ContentRepositoryBase.cs | 151 ++++++------------ .../Services/IContentServiceBase.cs | 14 +- .../Services/Implement/ContentService.cs | 14 +- .../Services/Implement/MediaService.cs | 15 +- src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../Checks/Data/DatabaseIntegrityCheck.cs | 53 +++--- .../PublishedCache/NuCache/ContentStore.cs | 2 +- 12 files changed, 163 insertions(+), 172 deletions(-) create mode 100644 src/Umbraco.Core/Models/ContentDataIntegrityReport.cs create mode 100644 src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs create mode 100644 src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 3ee2a75b09..7bce23e98e 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -66,7 +66,10 @@ namespace Umbraco.Core // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below // operations which will make this whole operation go much faster. When moving we don't need to create // new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.GetDirtyProperties().All(x => x == nameof(entity.Path) || x == nameof(entity.Level) || x == nameof(entity.UpdateDate)); + var isMoving = entity.IsPropertyDirty(nameof(entity.Path)) + && entity.IsPropertyDirty(nameof(entity.Level)) + && entity.IsPropertyDirty(nameof(entity.UpdateDate)); + return isMoving; } diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs new file mode 100644 index 0000000000..60683ee72f --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReport + { + public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) + { + DetectedIssues = detectedIssues; + } + + public bool Ok => DetectedIssues.Count == 0; + + public IReadOnlyDictionary DetectedIssues { get; } + + public enum IssueType + { + /// + /// The item's level and path are inconsistent with it's parent's path and level + /// + InvalidPathAndLevelByParentId, + + /// + /// The item's path doesn't contain all required parts + /// + InvalidPathEmpty, + + /// + /// The item's path parts are inconsistent with it's level value + /// + InvalidPathLevelMismatch, + + /// + /// The item's path does not end with it's own ID + /// + InvalidPathById, + + /// + /// The item's path does not have it's parent Id as the 2nd last entry + /// + InvalidPathByParentId, + } + } +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs new file mode 100644 index 0000000000..517b9e80dc --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReportEntry + { + public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) + { + IssueType = issueType; + } + + public ContentDataIntegrityReport.IssueType IssueType { get; } + public bool Fixed { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs new file mode 100644 index 0000000000..c4689467c1 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReportOptions + { + /// + /// Set to true to try to automatically resolve data integrity issues + /// + public bool FixIssues { get; set; } + + // TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc... + // things like Tag data consistency, etc... + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index aff7a58652..ad9e2d27c1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; @@ -78,11 +79,6 @@ namespace Umbraco.Core.Persistence.Repositories IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); - /// - /// Checks the data integrity of the node paths stored in the database - /// - bool VerifyNodePaths(out int[] invalidIds); - - void FixNodePaths(); + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index cf67244fbe..42d7e67bc0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -477,9 +477,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement IQuery filter, Ordering ordering); - public bool VerifyNodePaths(out int[] invalidIds) + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { - var invalid = new List(); + var report = new Dictionary(); var sql = SqlContext.Sql() .Select() @@ -487,80 +487,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .Where(x => x.NodeObjectType == NodeObjectTypeId) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - // TODO: Could verify sort orders here too - - var currentParentIds = new HashSet { -1 }; - var prevParentIds = currentParentIds; - var lastLevel = -1; - - // use a forward cursor (query) - foreach (var node in Database.Query(sql)) - { - if (node.Level != lastLevel) - { - // changing levels - prevParentIds = currentParentIds; - currentParentIds = null; - lastLevel = node.Level; - } - - if (currentParentIds == null) - { - // we're reset - currentParentIds = new HashSet(); - } - - currentParentIds.Add(node.NodeId); - - var pathParts = node.Path.Split(','); - - if (!prevParentIds.Contains(node.ParentId)) - { - // invalid, this will be because the level is wrong - invalid.Add(node.NodeId); - } - else if (pathParts.Length < 2) - { - // invalid path - invalid.Add(node.NodeId); - } - else if (pathParts.Length - 1 != node.Level) - { - // invalid, either path or level is wrong - invalid.Add(node.NodeId); - } - else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) - { - // invalid path - invalid.Add(node.NodeId); - } - else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString()) - { - // invalid path - invalid.Add(node.NodeId); - } - } - - invalidIds = invalid.ToArray(); - return invalid.Count == 0; - } - - public void FixNodePaths() - { - // TODO: We can probably combine this logic with the above - - var invalid = new List<(int child, int parent)>(); - - var sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // TODO: Could verify sort orders here too - - var updated = new List(); - var missingParentIds = new Dictionary>(); + var nodesToRebuild = new Dictionary>(); + var validNodes = new Dictionary(); var currentParentIds = new HashSet { -1 }; var prevParentIds = currentParentIds; var lastLevel = -1; @@ -589,54 +517,77 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!prevParentIds.Contains(node.ParentId)) { // invalid, this will be because the level is wrong (which prob means path is wrong too) - invalid.Add((node.NodeId, node.ParentId)); - if (missingParentIds.TryGetValue(node.ParentId, out var childIds)) - childIds.Add(node); - else - missingParentIds[node.ParentId] = new List {node}; + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); + AppendNodeToFix(nodesToRebuild, node); } else if (pathParts.Length < 2) { // invalid path - invalid.Add((node.NodeId, node.ParentId)); + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); + AppendNodeToFix(nodesToRebuild, node); } else if (pathParts.Length - 1 != node.Level) { // invalid, either path or level is wrong - invalid.Add((node.NodeId, node.ParentId)); + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch)); + AppendNodeToFix(nodesToRebuild, node); } else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) { // invalid path - invalid.Add((node.NodeId, node.ParentId)); + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById)); + AppendNodeToFix(nodesToRebuild, node); } else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString()) { // invalid path - invalid.Add((node.NodeId, node.ParentId)); + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId)); + AppendNodeToFix(nodesToRebuild, node); } else { - // it's valid + // it's valid! - if (missingParentIds.TryGetValue(node.NodeId, out var invalidNodes)) - { - // this parent has been flagged as missing which means one or more of it's children was ordered - // wrong and was checked first. So now we can try to rebuild the invalid paths. - - foreach (var invalidNode in invalidNodes) - { - invalidNode.Level = (short) (node.Level + 1); - invalidNode.Path = node.Path + "," + invalidNode.NodeId; - updated.Add(invalidNode); - } - } + // don't track unless we are configured to fix + if (options.FixIssues) + validNodes.Add(node.NodeId, node); } } - foreach (var node in updated) + var updated = new List(); + + if (options.FixIssues) { - Database.Update(node); + // iterate all valid nodes to see if these are parents for invalid nodes + foreach (var (nodeId, node) in validNodes) + { + if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue; + + // now we can try to rebuild the invalid paths. + + foreach (var invalidNode in invalidNodes) + { + invalidNode.Level = (short)(node.Level + 1); + invalidNode.Path = node.Path + "," + invalidNode.NodeId; + updated.Add(invalidNode); + } + } + + foreach (var node in updated) + { + Database.Update(node); + } + } + + return new ContentDataIntegrityReport(report); + + // inline method used to append nodes to rebuild + static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) + { + if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) + childIds.Add(node); + else + nodesToRebuild[node.ParentId] = new List { node }; } } diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs index 1c04e0b4a3..c40f49347f 100644 --- a/src/Umbraco.Core/Services/IContentServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentServiceBase.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Services +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services { /// /// Placeholder for sharing logic between the content, media (and member) services @@ -6,15 +8,9 @@ /// public interface IContentServiceBase : IService { - /// - /// Checks the data integrity of the node paths/levels stored in the database + /// Checks/fixes the data integrity of node paths/levels stored in the database /// - bool VerifyNodePaths(out int[] invalidIds); - - /// - /// Fixes the data integrity of node paths/levels stored in the database - /// - void FixNodePaths(); + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index f5f3d03abb..1f384d694f 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2383,23 +2383,13 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Succeed(evtMsgs); } - public bool VerifyNodePaths(out int[] invalidIds) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.VerifyNodePaths(out invalidIds); - } - } - - public void FixNodePaths() + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.WriteLock(Constants.Locks.ContentTree); - _documentRepository.FixNodePaths(); - // TODO: We're going to have to clear all caches + return _documentRepository.CheckDataIntegrity(options); } } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 219beb7088..4a2b37ca31 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1150,24 +1150,13 @@ namespace Umbraco.Core.Services.Implement } - - public bool VerifyNodePaths(out int[] invalidIds) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.MediaTree); - return _mediaRepository.VerifyNodePaths(out invalidIds); - } - } - - public void FixNodePaths() + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.WriteLock(Constants.Locks.MediaTree); - _mediaRepository.FixNodePaths(); - // TODO: We're going to have to clear all caches + return _mediaRepository.CheckDataIntegrity(options); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index efb76f2756..f15b098473 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,6 +132,9 @@ + + + diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs index d7bf62067f..e40be33fff 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -3,13 +3,15 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Serilog.Core; +using Umbraco.Core.Models; using Umbraco.Core.Services; namespace Umbraco.Web.HealthCheck.Checks.Data { [HealthCheck( "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", - "Database integrity check", + "Database data integrity check", Description = "Checks for various data integrity issues in the Umbraco database.", Group = "Data Integrity")] public class DatabaseIntegrityCheck : HealthCheck @@ -18,6 +20,8 @@ namespace Umbraco.Web.HealthCheck.Checks.Data private readonly IMediaService _mediaService; private const string _fixMediaPaths = "fixMediaPaths"; private const string _fixContentPaths = "fixContentPaths"; + private const string _fixMediaPathsTitle = "Fix media paths"; + private const string _fixContentPathsTitle = "Fix content paths"; public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService) { @@ -34,35 +38,29 @@ namespace Umbraco.Web.HealthCheck.Checks.Data //return the statuses return new[] { - CheckContent(), - CheckMedia() + CheckDocuments(false), + CheckMedia(false) }; } - private HealthCheckStatus CheckMedia() + private HealthCheckStatus CheckMedia(bool fix) { - return CheckPaths(_fixMediaPaths, "Fix media paths", "media", () => - { - var mediaPaths = _mediaService.VerifyNodePaths(out var invalidMediaPaths); - return (mediaPaths, invalidMediaPaths); - }); + return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, + () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); } - private HealthCheckStatus CheckContent() + private HealthCheckStatus CheckDocuments(bool fix) { - return CheckPaths(_fixContentPaths, "Fix content paths", "content", () => - { - var contentPaths = _contentService.VerifyNodePaths(out var invalidContentPaths); - return (contentPaths, invalidContentPaths); - }); + return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, + () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); } - private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, Func<(bool success, int[] invalidPaths)> doCheck) + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, Func doCheck) { var result = doCheck(); var actions = new List(); - if (!result.success) + if (!result.Ok) { actions.Add(new HealthCheckAction(actionAlias, Id) { @@ -70,28 +68,23 @@ namespace Umbraco.Web.HealthCheck.Checks.Data }); } - return new HealthCheckStatus(result.success + return new HealthCheckStatus(result.Ok ? $"All {entityType} paths are valid" - : $"There are {result.invalidPaths.Length} invalid {entityType} paths") + : $"There are {result.DetectedIssues.Count} invalid {entityType} paths") { - ResultType = result.success ? StatusResultType.Success : StatusResultType.Error, + ResultType = result.Ok ? StatusResultType.Success : StatusResultType.Error, Actions = actions }; } public override HealthCheckStatus ExecuteAction(HealthCheckAction action) { - switch (action.Alias) + return action.Alias switch { - case _fixContentPaths: - _contentService.FixNodePaths(); - return CheckContent(); - case _fixMediaPaths: - _mediaService.FixNodePaths(); - return CheckMedia(); - default: - throw new InvalidOperationException("Action not supported"); - } + _fixContentPaths => CheckDocuments(true), + _fixMediaPaths => CheckMedia(true), + _ => throw new InvalidOperationException("Action not supported") + }; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 474c390027..e40e7130d0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -525,7 +525,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // because the data sort operation is by path. if (parent.Value == null) { - _logger.Warn($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}."); + _logger.Warn($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}. See the Health Check dashboard in Settings to resolve data integrity issues."); return false; } From 3ef188376517a8afc024d2e4b39170fe351013fd Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Apr 2020 16:53:54 +1000 Subject: [PATCH 12/74] fix build (non latest c#) --- .../Implement/ContentRepositoryBase.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 42d7e67bc0..93c4078f46 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -580,15 +580,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } return new ContentDataIntegrityReport(report); + } - // inline method used to append nodes to rebuild - static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) - { - if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) - childIds.Add(node); - else - nodesToRebuild[node.ParentId] = new List { node }; - } + private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) + { + if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) + childIds.Add(node); + else + nodesToRebuild[node.ParentId] = new List { node }; } // here, filter can be null and ordering cannot From 19f0793cba726ce03acea4da15ed1f4ff59bc09c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Apr 2020 17:38:40 +1000 Subject: [PATCH 13/74] updates health check --- .../Implement/ContentRepositoryBase.cs | 12 +++--- .../Checks/Data/DatabaseIntegrityCheck.cs | 38 +++++++++++++++---- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 93c4078f46..9999e9f2d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -489,7 +489,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var nodesToRebuild = new Dictionary>(); var validNodes = new Dictionary(); - var currentParentIds = new HashSet { -1 }; + var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia}; + var currentParentIds = new HashSet(rootIds); var prevParentIds = currentParentIds; var lastLevel = -1; @@ -512,7 +513,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement currentParentIds.Add(node.NodeId); - var pathParts = node.Path.Split(','); + // paths parts without the roots + var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray(); if (!prevParentIds.Contains(node.ParentId)) { @@ -520,13 +522,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); AppendNodeToFix(nodesToRebuild, node); } - else if (pathParts.Length < 2) + else if (pathParts.Length == 0) { // invalid path report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); AppendNodeToFix(nodesToRebuild, node); } - else if (pathParts.Length - 1 != node.Level) + else if (pathParts.Length != node.Level) { // invalid, either path or level is wrong report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch)); @@ -538,7 +540,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById)); AppendNodeToFix(nodesToRebuild, node); } - else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString()) + else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString()) { // invalid path report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId)); diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs index e40be33fff..876ad9f1d7 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -45,22 +45,22 @@ namespace Umbraco.Web.HealthCheck.Checks.Data private HealthCheckStatus CheckMedia(bool fix) { - return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, + return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, fix, () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); } private HealthCheckStatus CheckDocuments(bool fix) { - return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, + return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, fix, () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); } - private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, Func doCheck) + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) { - var result = doCheck(); + var report = doCheck(); var actions = new List(); - if (!result.Ok) + if (!report.Ok) { actions.Add(new HealthCheckAction(actionAlias, Id) { @@ -68,15 +68,37 @@ namespace Umbraco.Web.HealthCheck.Checks.Data }); } - return new HealthCheckStatus(result.Ok + return new HealthCheckStatus(report.Ok ? $"All {entityType} paths are valid" - : $"There are {result.DetectedIssues.Count} invalid {entityType} paths") + : GetInvalidReport(report, entityType, detailedReport)) { - ResultType = result.Ok ? StatusResultType.Success : StatusResultType.Error, + ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, Actions = actions }; } + private static string GetInvalidReport(ContentDataIntegrityReport report, string entityType, bool detailed) + { + var sb = new StringBuilder(); + sb.AppendLine($"There are {report.DetectedIssues.Count} invalid {entityType} paths"); + + if (true && report.DetectedIssues.Count > 0) + { + sb.AppendLine("
    "); + foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) + { + var countByGroup = issueGroup.Count(); + var fixedByGroup = issueGroup.Count(x => x.Value.Fixed); + sb.AppendLine("
  • "); + sb.AppendLine($"{countByGroup} issues of type {issueGroup.Key} ... {fixedByGroup} fixed"); + sb.AppendLine("
  • "); + } + sb.AppendLine("
"); + } + + return sb.ToString(); + } + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) { return action.Alias switch From 49c6fd2c76f76f09f7f14868ad827a67a1af6c3d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Apr 2020 10:38:02 +1000 Subject: [PATCH 14/74] data integrity report working --- .../Models/ContentDataIntegrityReport.cs | 3 ++- .../Implement/ContentRepositoryBase.cs | 2 ++ .../Checks/Data/DatabaseIntegrityCheck.cs | 21 +++++++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs index 60683ee72f..d3660679a7 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Models { @@ -9,7 +10,7 @@ namespace Umbraco.Core.Models DetectedIssues = detectedIssues; } - public bool Ok => DetectedIssues.Count == 0; + public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); public IReadOnlyDictionary DetectedIssues { get; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 9999e9f2d6..4aa9655249 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -578,6 +578,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var node in updated) { Database.Update(node); + if (report.TryGetValue(node.NodeId, out var entry)) + entry.Fixed = true; } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs index 876ad9f1d7..b7d9aea3b9 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -68,21 +68,30 @@ namespace Umbraco.Web.HealthCheck.Checks.Data }); } - return new HealthCheckStatus(report.Ok - ? $"All {entityType} paths are valid" - : GetInvalidReport(report, entityType, detailedReport)) + return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) { ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, Actions = actions }; } - private static string GetInvalidReport(ContentDataIntegrityReport report, string entityType, bool detailed) + private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) { var sb = new StringBuilder(); - sb.AppendLine($"There are {report.DetectedIssues.Count} invalid {entityType} paths"); - if (true && report.DetectedIssues.Count > 0) + if (report.Ok) + { + sb.AppendLine($"

All {entityType} paths are valid

"); + + if (!detailed) + return sb.ToString(); + } + else + { + sb.AppendLine($"

{report.DetectedIssues.Count} invalid {entityType} paths detected.

"); + } + + if (detailed && report.DetectedIssues.Count > 0) { sb.AppendLine("
    "); foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) From d54eed5cb9396d23094bf712458e23588703f341 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Apr 2020 10:54:48 +1000 Subject: [PATCH 15/74] argh! can't use switch expressions in v8 --- .../Checks/Data/DatabaseIntegrityCheck.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs index b7d9aea3b9..0c3e2f3d91 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -110,12 +110,15 @@ namespace Umbraco.Web.HealthCheck.Checks.Data public override HealthCheckStatus ExecuteAction(HealthCheckAction action) { - return action.Alias switch + switch (action.Alias) { - _fixContentPaths => CheckDocuments(true), - _fixMediaPaths => CheckMedia(true), - _ => throw new InvalidOperationException("Action not supported") - }; + case _fixContentPaths: + return CheckDocuments(true); + case _fixMediaPaths: + return CheckMedia(true); + default: + throw new InvalidOperationException("Action not supported"); + } } } } From 36c9cd47a106a14b1944f708ae96ea933aa6181a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Apr 2020 11:22:15 +1000 Subject: [PATCH 16/74] Adds notes --- .../Repositories/Implement/DocumentRepository.cs | 4 ++++ src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index a75165a7b9..db1e2b350d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -538,6 +538,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. var isMoving = entity.IsMoving(); + // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. + // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost + // copy performance by 95% just like we did for Move + var publishing = entity.PublishedState == PublishedState.Publishing; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index e40e7130d0..b5fc1abd7f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -817,7 +817,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen ClearBranchLocked(existing); - //TODO: This removes the current GEN from the tree - do we really want to do that? + //TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....) RemoveTreeNodeLocked(existing); } @@ -882,6 +882,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void ClearBranchLocked(ContentNode content) { + // This should never be null, all code that calls this method is null checking but we've seen + // issues of null ref exceptions in issue reports so we'll double check here + if (content == null) throw new ArgumentNullException(nameof(content)); + SetValueLocked(_contentNodes, content.Id, null); if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null); @@ -890,8 +894,9 @@ namespace Umbraco.Web.PublishedCache.NuCache var id = content.FirstChildContentId; while (id > 0) { + // get the required link node, this ensures that both `link` and `link.Value` are not null var link = GetRequiredLinkedNode(id, "child", null); - ClearBranchLocked(link.Value); + ClearBranchLocked(link.Value); // recurse id = link.Value.NextSiblingContentId; } } From 64e7a29795cd62bf5253ab997250a510a84f07bc Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Apr 2020 15:03:21 +1000 Subject: [PATCH 17/74] Ensures caches are refreshed after data errors are fixed --- .../Models/ContentDataIntegrityReport.cs | 3 +++ .../Services/Implement/ContentService.cs | 13 +++++++++++-- src/Umbraco.Core/Services/Implement/MediaService.cs | 13 +++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs index d3660679a7..9f0f147083 100644 --- a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -14,6 +14,9 @@ namespace Umbraco.Core.Models public IReadOnlyDictionary DetectedIssues { get; } + public IReadOnlyDictionary FixedIssues + => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); + public enum IssueType { /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1f384d694f..068864a558 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2388,8 +2388,17 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.WriteLock(Constants.Locks.ContentTree); - // TODO: We're going to have to clear all caches - return _documentRepository.CheckDataIntegrity(options); + + var report = _documentRepository.CheckDataIntegrity(options); + + if (report.FixedIssues.Count > 0) + { + //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref + var root = new Content("root", -1, new ContentType(-1)) {Id = -1, Key = Guid.Empty}; + scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); + } + + return report; } } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 4a2b37ca31..ecd4cccc8d 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1155,8 +1155,17 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.WriteLock(Constants.Locks.MediaTree); - // TODO: We're going to have to clear all caches - return _mediaRepository.CheckDataIntegrity(options); + + var report = _mediaRepository.CheckDataIntegrity(options); + + if (report.FixedIssues.Count > 0) + { + //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref + var root = new Models.Media("root", -1, new MediaType(-1)) { Id = -1, Key = Guid.Empty }; + scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); + } + + return report; } } From 5f5773b0c02e2190ac7d94b3ce84008c9a85a63c Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 1 Apr 2020 16:22:20 -0700 Subject: [PATCH 18/74] 7879: Handle for multiple picked media relations (cherry picked from commit 9f7c44c64ebfb23acd1bb06aa22c5bc39ad5079a) --- .../PropertyEditors/MediaPickerPropertyEditor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index ece210b9d1..fa8060bd15 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -45,8 +45,11 @@ namespace Umbraco.Web.PropertyEditors if (string.IsNullOrEmpty(asString)) yield break; - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); + foreach (var udiStr in asString.Split(',')) + { + if (Udi.TryParse(udiStr, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } } From d0eee32f1d184da7a8eb8207482ba90c3130bd9c Mon Sep 17 00:00:00 2001 From: mattchanner Date: Wed, 1 Apr 2020 13:12:05 +0100 Subject: [PATCH 19/74] Added null check on raw JSON to prevent null reference error deserializing the grid (cherry picked from commit bcea0f645a1f39b13bedbac0f6b4b6f6b6d6a9e0) --- src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 7134fe8703..6606e5d100 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -165,6 +165,10 @@ namespace Umbraco.Web.PropertyEditors public IEnumerable GetReferences(object value) { var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (rawJson.IsNullOrWhiteSpace()) + yield break; + DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => From b3fd94e023acd1104df024a2aa284fe5ec3b9a91 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 11 Apr 2020 16:35:33 +0200 Subject: [PATCH 20/74] Migrated user tests into new project and builder pattern. --- .../Builders/Extensions/BuilderExtensions.cs | 55 ++++++ .../Builders/Interfaces/IAccountBuilder.cs | 12 ++ .../Interfaces/IWithApprovedBuilder.cs | 7 - .../Builders/Interfaces/IWithEmailBuilder.cs | 7 + .../IWithFailedPasswordAttemptsBuilder.cs | 7 + .../Interfaces/IWithIsApprovedBuilder.cs | 7 + .../Interfaces/IWithIsLockedOutBuilder.cs | 11 ++ .../Interfaces/IWithLastLoginDateBuilder.cs | 9 + .../IWithLastPasswordChangeDateBuilder.cs | 9 + .../Builders/Interfaces/IWithLoginBuilder.cs | 9 + .../Builders/MemberBuilder.cs | 123 +++++++------- .../Builders/UserBuilder.cs | 158 ++++++++++++++---- .../Models/MemberTests.cs | 6 +- .../Models/UserTests.cs | 61 +++++++ .../Builders/MemberBuilderTests.cs | 15 +- .../Builders/UserBuilderTests.cs | 74 ++++++++ src/Umbraco.Tests/Models/UserTests.cs | 90 ---------- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - 18 files changed, 464 insertions(+), 197 deletions(-) create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IAccountBuilder.cs delete mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithApprovedBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithEmailBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithFailedPasswordAttemptsBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsApprovedBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsLockedOutBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastLoginDateBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastPasswordChangeDateBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/Interfaces/IWithLoginBuilder.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserBuilderTests.cs delete mode 100644 src/Umbraco.Tests/Models/UserTests.cs diff --git a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs index c105a8bded..3228181edb 100644 --- a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs @@ -116,5 +116,60 @@ namespace Umbraco.Tests.Common.Builders.Extensions builder.Thumbnail = thumbnail; return builder; } + + public static T WithLogin(this T builder, string username, string rawPasswordValue) + where T : IWithLoginBuilder + { + builder.Username = username; + builder.RawPasswordValue = rawPasswordValue; + return builder; + } + + public static T WithEmail(this T builder, string email) + where T : IWithEmailBuilder + { + builder.Email = email; + return builder; + } + + public static T WithFailedPasswordAttempts(this T builder, int failedPasswordAttempts) + where T : IWithFailedPasswordAttemptsBuilder + { + builder.FailedPasswordAttempts = failedPasswordAttempts; + return builder; + } + + public static T WithIsApproved(this T builder, bool isApproved) + where T : IWithIsApprovedBuilder + { + builder.IsApproved = isApproved; + return builder; + } + + public static T WithIsLockedOut(this T builder, bool isLockedOut, DateTime? lastLockoutDate = null) + where T : IWithIsLockedOutBuilder + { + builder.IsLockedOut = isLockedOut; + if (lastLockoutDate.HasValue) + { + builder.LastLockoutDate = lastLockoutDate.Value; + } + + return builder; + } + + public static T WithLastLoginDate(this T builder, DateTime lastLoginDate) + where T : IWithLastLoginDateBuilder + { + builder.LastLoginDate = lastLoginDate; + return builder; + } + + public static T WithLastPasswordChangeDate(this T builder, DateTime lastPasswordChangeDate) + where T : IWithLastPasswordChangeDateBuilder + { + builder.LastPasswordChangeDate = lastPasswordChangeDate; + return builder; + } } } diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IAccountBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IAccountBuilder.cs new file mode 100644 index 0000000000..77fdd30547 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IAccountBuilder.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IAccountBuilder : IWithLoginBuilder, + IWithEmailBuilder, + IWithFailedPasswordAttemptsBuilder, + IWithIsApprovedBuilder, + IWithIsLockedOutBuilder, + IWithLastLoginDateBuilder, + IWithLastPasswordChangeDateBuilder + { + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithApprovedBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithApprovedBuilder.cs deleted file mode 100644 index 48d41e7a84..0000000000 --- a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithApprovedBuilder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Umbraco.Tests.Common.Builders.Interfaces -{ - public interface IWithApprovedBuilder - { - bool? Approved { get; set; } - } -} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithEmailBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithEmailBuilder.cs new file mode 100644 index 0000000000..cb3be57e62 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithEmailBuilder.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithEmailBuilder + { + string Email { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithFailedPasswordAttemptsBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithFailedPasswordAttemptsBuilder.cs new file mode 100644 index 0000000000..d39647b28e --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithFailedPasswordAttemptsBuilder.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithFailedPasswordAttemptsBuilder + { + int? FailedPasswordAttempts { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsApprovedBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsApprovedBuilder.cs new file mode 100644 index 0000000000..c27c34d3a2 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsApprovedBuilder.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithIsApprovedBuilder + { + bool? IsApproved { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsLockedOutBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsLockedOutBuilder.cs new file mode 100644 index 0000000000..55577ed312 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithIsLockedOutBuilder.cs @@ -0,0 +1,11 @@ +using System; + +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithIsLockedOutBuilder + { + bool? IsLockedOut { get; set; } + + DateTime? LastLockoutDate { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastLoginDateBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastLoginDateBuilder.cs new file mode 100644 index 0000000000..0355786927 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastLoginDateBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithLastLoginDateBuilder + { + DateTime? LastLoginDate { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastPasswordChangeDateBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastPasswordChangeDateBuilder.cs new file mode 100644 index 0000000000..833da022d4 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLastPasswordChangeDateBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithLastPasswordChangeDateBuilder + { + DateTime? LastPasswordChangeDate { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLoginBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLoginBuilder.cs new file mode 100644 index 0000000000..dc099f55b9 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithLoginBuilder.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Tests.Common.Builders.Interfaces +{ + public interface IWithLoginBuilder + { + string Username { get; set; } + + string RawPasswordValue { get; set; } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs index 3925ac642b..16ec7ed005 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs @@ -15,7 +15,8 @@ namespace Umbraco.Tests.Common.Builders IWithTrashedBuilder, IWithLevelBuilder, IWithPathBuilder, - IWithSortOrderBuilder + IWithSortOrderBuilder, + IAccountBuilder { private MemberTypeBuilder _memberTypeBuilder; private GenericCollectionBuilder _memberGroupsBuilder; @@ -28,12 +29,12 @@ namespace Umbraco.Tests.Common.Builders private DateTime? _updateDate; private string _name; private int? _creatorId; + private int? _level; + private string _path; private string _username; private string _rawPasswordValue; private string _email; private int? _failedPasswordAttempts; - private int? _level; - private string _path; private bool? _isApproved; private bool? _isLockedOut; private DateTime? _lastLockoutDate; @@ -43,60 +44,6 @@ namespace Umbraco.Tests.Common.Builders private bool? _trashed; private int? _propertyIdsIncrementingFrom; - public MemberBuilder WithUserName(string username) - { - _username = username; - return this; - } - - public MemberBuilder WithEmail(string email) - { - _email = email; - return this; - } - - public MemberBuilder WithRawPasswordValue(string rawPasswordValue) - { - _rawPasswordValue = rawPasswordValue; - return this; - } - - public MemberBuilder WithFailedPasswordAttempts(int failedPasswordAttempts) - { - _failedPasswordAttempts = failedPasswordAttempts; - return this; - } - - public MemberBuilder WithIsApproved(bool isApproved) - { - _isApproved = isApproved; - return this; - } - - public MemberBuilder WithIsLockedOut(bool isLockedOut) - { - _isLockedOut = isLockedOut; - return this; - } - - public MemberBuilder WithLastLockoutDate(DateTime lastLockoutDate) - { - _lastLockoutDate = lastLockoutDate; - return this; - } - - public MemberBuilder WithLastLoginDate(DateTime lastLoginDate) - { - _lastLoginDate = lastLoginDate; - return this; - } - - public MemberBuilder WithLastPasswordChangeDate(DateTime lastPasswordChangeDate) - { - _lastPasswordChangeDate = lastPasswordChangeDate; - return this; - } - public MemberBuilder WithPropertyIdsIncrementingFrom(int propertyIdsIncrementingFrom) { _propertyIdsIncrementingFrom = propertyIdsIncrementingFrom; @@ -139,19 +86,19 @@ namespace Umbraco.Tests.Common.Builders var updateDate = _updateDate ?? DateTime.Now; var name = _name ?? Guid.NewGuid().ToString(); var creatorId = _creatorId ?? 1; + var level = _level ?? 1; + var path = _path ?? "-1"; + var sortOrder = _sortOrder ?? 0; + var trashed = _trashed ?? false; var username = _username ?? string.Empty; var email = _email ?? string.Empty; var rawPasswordValue = _rawPasswordValue ?? string.Empty; var failedPasswordAttempts = _failedPasswordAttempts ?? 0; - var level = _level ?? 1; - var path = _path ?? "-1"; var isApproved = _isApproved ?? false; var isLockedOut = _isLockedOut ?? false; var lastLockoutDate = _lastLockoutDate ?? DateTime.Now; var lastLoginDate = _lastLoginDate ?? DateTime.Now; var lastPasswordChangeDate = _lastPasswordChangeDate ?? DateTime.Now; - var sortOrder = _sortOrder ?? 0; - var trashed = _trashed ?? false; if (_memberTypeBuilder == null) { @@ -276,5 +223,59 @@ namespace Umbraco.Tests.Common.Builders get => _sortOrder; set => _sortOrder = value; } + + string IWithLoginBuilder.Username + { + get => _username; + set => _username = value; + } + + string IWithLoginBuilder.RawPasswordValue + { + get => _rawPasswordValue; + set => _rawPasswordValue = value; + } + + string IWithEmailBuilder.Email + { + get => _email; + set => _email = value; + } + + int? IWithFailedPasswordAttemptsBuilder.FailedPasswordAttempts + { + get => _failedPasswordAttempts; + set => _failedPasswordAttempts = value; + } + + bool? IWithIsApprovedBuilder.IsApproved + { + get => _isApproved; + set => _isApproved = value; + } + + bool? IWithIsLockedOutBuilder.IsLockedOut + { + get => _isLockedOut; + set => _isLockedOut = value; + } + + DateTime? IWithIsLockedOutBuilder.LastLockoutDate + { + get => _lastLockoutDate; + set => _lastLockoutDate = value; + } + + DateTime? IWithLastLoginDateBuilder.LastLoginDate + { + get => _lastLoginDate; + set => _lastLoginDate = value; + } + + DateTime? IWithLastPasswordChangeDateBuilder.LastPasswordChangeDate + { + get => _lastPasswordChangeDate; + set => _lastPasswordChangeDate = value; + } } } diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs index cb24e0e8dc..4fafb3e1bd 100644 --- a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -1,10 +1,9 @@ -using Umbraco.Configuration.Models; +using System; using Umbraco.Core.Models.Membership; using Umbraco.Tests.Common.Builders.Interfaces; namespace Umbraco.Tests.Common.Builders { - public class UserBuilder : UserBuilder { public UserBuilder() : base(null) @@ -15,27 +14,57 @@ namespace Umbraco.Tests.Common.Builders public class UserBuilder : ChildBuilderBase, IWithIdBuilder, + IWithKeyBuilder, + IWithCreateDateBuilder, + IWithUpdateDateBuilder, IWithNameBuilder, - IWithApprovedBuilder + IAccountBuilder { private int? _id; + private Guid? _key; + private DateTime? _createDate; + private DateTime? _updateDate; private string _language; - private bool? _approved; private string _name; - private string _rawPassword; - private bool? _isLockedOut; - private string _email; private string _username; + private string _rawPasswordValue; + private string _email; + private int? _failedPasswordAttempts; + private bool? _isApproved; + private bool? _isLockedOut; + private DateTime? _lastLockoutDate; + private DateTime? _lastLoginDate; + private DateTime? _lastPasswordChangeDate; private string _suffix = string.Empty; private string _defaultLang; - + private string _comments; + private int? _sessionTimeout; + private int[] _startContentIds; + private int[] _startMediaIds; public UserBuilder(TParent parentBuilder) : base(parentBuilder) { - } - public UserBuilder WithDefaultUILanguage(string defaultLang) + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + DateTime? IWithCreateDateBuilder.CreateDate + { + get => _createDate; + set => _createDate = value; + } + + DateTime? IWithUpdateDateBuilder.UpdateDate + { + get => _updateDate; + set => _updateDate = value; + } + + public UserBuilder WithDefaultUILanguage(string defaultLang) { _defaultLang = defaultLang; return this; @@ -47,27 +76,27 @@ namespace Umbraco.Tests.Common.Builders return this; } - public UserBuilder WithRawPassword(string rawPassword) + public UserBuilder WithComments(string comments) { - _rawPassword = rawPassword; + _comments = comments; return this; } - public UserBuilder WithEmail(string email) + public UserBuilder WithSessionTimeout(int sessionTimeout) { - _email = email; + _sessionTimeout = sessionTimeout; return this; } - public UserBuilder WithUsername(string username) + public UserBuilder WithStartContentIds(int[] startContentIds) { - _username = username; + _startContentIds = startContentIds; return this; } - public UserBuilder WithLockedOut(bool isLockedOut) + public UserBuilder WithStartMediaIds(int[] startMediaIds) { - _isLockedOut = isLockedOut; + _startMediaIds = startMediaIds; return this; } @@ -84,25 +113,50 @@ namespace Umbraco.Tests.Common.Builders public override User Build() { - var globalSettings = new GlobalSettingsBuilder().WithDefaultUiLanguage(_defaultLang).Build(); + var id = _id ?? 1; + var defaultLang = _defaultLang ?? "en"; + var globalSettings = new GlobalSettingsBuilder().WithDefaultUiLanguage(defaultLang).Build(); + var key = _key ?? Guid.NewGuid(); + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; var name = _name ?? "TestUser" + _suffix; - var email = _email ?? "test" + _suffix + "@test.com"; - var username = _username ?? "TestUser" + _suffix; - var rawPassword = _rawPassword ?? "abcdefghijklmnopqrstuvwxyz"; var language = _language ?? globalSettings.DefaultUILanguage; + var username = _username ?? "TestUser" + _suffix; + var email = _email ?? "test" + _suffix + "@test.com"; + var rawPasswordValue = _rawPasswordValue ?? "abcdefghijklmnopqrstuvwxyz"; + var failedPasswordAttempts = _failedPasswordAttempts ?? 0; + var isApproved = _isApproved ?? false; var isLockedOut = _isLockedOut ?? false; - var approved = _approved ?? true; + var lastLockoutDate = _lastLockoutDate ?? DateTime.Now; + var lastLoginDate = _lastLoginDate ?? DateTime.Now; + var lastPasswordChangeDate = _lastPasswordChangeDate ?? DateTime.Now; + var comments = _comments ?? string.Empty; + var sessionTimeout = _sessionTimeout ?? 0; + var startContentIds = _startContentIds ?? new int[0]; + var startMediaIds = _startMediaIds ?? new int[0]; return new User( globalSettings, name, email, username, - rawPassword) + rawPasswordValue) { + Id = id, + Key = key, + CreateDate = createDate, + UpdateDate = updateDate, Language = language, + FailedPasswordAttempts = failedPasswordAttempts, + IsApproved = isApproved, IsLockedOut = isLockedOut, - IsApproved = approved + LastLockoutDate = lastLockoutDate, + LastLoginDate = lastLoginDate, + LastPasswordChangeDate = lastPasswordChangeDate, + Comments = comments, + SessionTimeout = sessionTimeout, + StartContentIds = startContentIds, + StartMediaIds = startMediaIds, }; } @@ -118,10 +172,58 @@ namespace Umbraco.Tests.Common.Builders set => _name = value; } - bool? IWithApprovedBuilder.Approved + string IWithLoginBuilder.Username { - get => _approved; - set => _approved = value; + get => _username; + set => _username = value; + } + + string IWithLoginBuilder.RawPasswordValue + { + get => _rawPasswordValue; + set => _rawPasswordValue = value; + } + + string IWithEmailBuilder.Email + { + get => _email; + set => _email = value; + } + + int? IWithFailedPasswordAttemptsBuilder.FailedPasswordAttempts + { + get => _failedPasswordAttempts; + set => _failedPasswordAttempts = value; + } + + bool? IWithIsApprovedBuilder.IsApproved + { + get => _isApproved; + set => _isApproved = value; + } + + bool? IWithIsLockedOutBuilder.IsLockedOut + { + get => _isLockedOut; + set => _isLockedOut = value; + } + + DateTime? IWithIsLockedOutBuilder.LastLockoutDate + { + get => _lastLockoutDate; + set => _lastLockoutDate = value; + } + + DateTime? IWithLastLoginDateBuilder.LastLoginDate + { + get => _lastLoginDate; + set => _lastLoginDate = value; + } + + DateTime? IWithLastPasswordChangeDateBuilder.LastPasswordChangeDate + { + get => _lastPasswordChangeDate; + set => _lastPasswordChangeDate = value; } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs index 1ac255db39..85cd84526a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs @@ -116,14 +116,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models .Done() .Done() .WithId(10) - .WithKey(Guid.NewGuid()) .WithName("Fred") - .WithUserName("fred") - .WithRawPasswordValue("raw pass") + .WithLogin("fred", "raw pass") .WithEmail("email@email.com") .WithCreatorId(22) - .WithCreateDate(DateTime.Now) - .WithUpdateDate(DateTime.Now) .WithFailedPasswordAttempts(22) .WithLevel(3) .WithPath("-1, 4, 10") diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs new file mode 100644 index 0000000000..281f8cd040 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core.Models.Membership; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Common.Builders.Extensions; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models +{ + [TestFixture] + public class UserTests + { + private readonly UserBuilder _builder = new UserBuilder(); + + [Test] + public void Can_Deep_Clone() + { + var item = BuildUser(); + + var clone = (User)item.DeepClone(); + + Assert.AreNotSame(clone, item); + Assert.AreEqual(clone, item); + + Assert.AreEqual(clone.AllowedSections.Count(), item.AllowedSections.Count()); + + //Verify normal properties with reflection + var allProps = clone.GetType().GetProperties(); + foreach (var propertyInfo in allProps) + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); + } + + [Test] + public void Can_Serialize_Without_Error() + { + var item = BuildUser(); + + var json = JsonConvert.SerializeObject(item); + Debug.Print(json); + } + + private User BuildUser() + { + return _builder + .WithId(3) + .WithLogin("username", "test pass") + .WithName("Test") + .WithEmail("test@test.com") + .WithFailedPasswordAttempts(3) + .WithIsApproved(true) + .WithIsLockedOut(true) + .WithComments("comments") + .WithSessionTimeout(5) + .WithStartContentIds(new[] { 3 }) + .WithStartMediaIds(new[] { 8 }) + .Build(); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MemberBuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MemberBuilderTests.cs index 42d239eff6..386db34a74 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MemberBuilderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MemberBuilderTests.cs @@ -48,6 +48,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Tests.Common.Builders var testKey = Guid.NewGuid(); var testCreateDate = DateTime.Now.AddHours(-1); var testUpdateDate = DateTime.Now; + const int testFailedPasswordAttempts = 22; var testLastLockoutDate = DateTime.Now.AddHours(-2); var testLastLoginDate = DateTime.Now.AddHours(-3); var testLastPasswordChangeDate = DateTime.Now.AddHours(-4); @@ -104,18 +105,16 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Tests.Common.Builders .WithId(testId) .WithKey(testKey) .WithName(testName) - .WithUserName(testUsername) - .WithRawPasswordValue(testRawPasswordValue) + .WithLogin(testUsername, testRawPasswordValue) .WithEmail(testEmail) .WithCreatorId(testCreatorId) .WithCreateDate(testCreateDate) .WithUpdateDate(testUpdateDate) - .WithFailedPasswordAttempts(22) .WithLevel(testLevel) .WithPath(testPath) + .WithFailedPasswordAttempts(testFailedPasswordAttempts) .WithIsApproved(testIsApproved) - .WithIsLockedOut(testIsLockedOut) - .WithLastLockoutDate(testLastLockoutDate) + .WithIsLockedOut(testIsLockedOut, testLastLockoutDate) .WithLastLoginDate(testLastLoginDate) .WithLastPasswordChangeDate(testLastPasswordChangeDate) .WithSortOrder(testSortOrder) @@ -146,6 +145,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Tests.Common.Builders Assert.AreEqual(testCreateDate, member.CreateDate); Assert.AreEqual(testUpdateDate, member.UpdateDate); Assert.AreEqual(testCreatorId, member.CreatorId); + Assert.AreEqual(testFailedPasswordAttempts, member.FailedPasswordAttempts); + Assert.AreEqual(testIsApproved, member.IsApproved); + Assert.AreEqual(testIsLockedOut, member.IsLockedOut); + Assert.AreEqual(testLastLockoutDate, member.LastLockoutDate); + Assert.AreEqual(testLastLoginDate, member.LastLoginDate); + Assert.AreEqual(testLastPasswordChangeDate, member.LastPasswordChangeDate); Assert.AreEqual(testGroups, member.Groups.ToArray()); Assert.AreEqual(10, member.Properties.Count); // 7 from membership properties group, 3 custom Assert.AreEqual(testPropertyData1.Value, member.GetValue(testPropertyData1.Key)); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserBuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserBuilderTests.cs new file mode 100644 index 0000000000..5b3b0595a3 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserBuilderTests.cs @@ -0,0 +1,74 @@ +using System; +using NUnit.Framework; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Common.Builders.Extensions; + +namespace Umbraco.Tests.UnitTests.Umbraco.Tests.Common.Builders +{ + [TestFixture] + public class UserBuilderTests + { + [Test] + public void Is_Built_Correctly() + { + // Arrange + const int testId = 10; + const string testName = "Fred"; + const string testUsername = "fred"; + const string testRawPasswordValue = "raw pass"; + const string testEmail = "email@email.com"; + const bool testIsApproved = true; + const bool testIsLockedOut = true; + var testKey = Guid.NewGuid(); + var testCreateDate = DateTime.Now.AddHours(-1); + var testUpdateDate = DateTime.Now; + const int testFailedPasswordAttempts = 22; + var testLastLockoutDate = DateTime.Now.AddHours(-2); + var testLastLoginDate = DateTime.Now.AddHours(-3); + var testLastPasswordChangeDate = DateTime.Now.AddHours(-4); + var testComments = "comments"; + var testSessionTimeout = 5; + var testStartContentIds = new[] { 3 }; + var testStartMediaIds = new[] { 8 }; + + var builder = new UserBuilder(); + + // Act + var user = builder + .WithId(testId) + .WithKey(testKey) + .WithName(testName) + .WithLogin(testUsername, testRawPasswordValue) + .WithEmail(testEmail) + .WithCreateDate(testCreateDate) + .WithUpdateDate(testUpdateDate) + .WithFailedPasswordAttempts(testFailedPasswordAttempts) + .WithIsApproved(testIsApproved) + .WithIsLockedOut(testIsLockedOut, testLastLockoutDate) + .WithLastLoginDate(testLastLoginDate) + .WithLastPasswordChangeDate(testLastPasswordChangeDate) + .WithComments(testComments) + .WithSessionTimeout(5) + .WithStartContentIds(new[] { 3 }) + .WithStartMediaIds(new[] { 8 }) + .Build(); + + // Assert + Assert.AreEqual(testId, user.Id); + Assert.AreEqual(testKey, user.Key); + Assert.AreEqual(testName, user.Name); + Assert.AreEqual(testCreateDate, user.CreateDate); + Assert.AreEqual(testUpdateDate, user.UpdateDate); + Assert.AreEqual(testFailedPasswordAttempts, user.FailedPasswordAttempts); + Assert.AreEqual(testIsApproved, user.IsApproved); + Assert.AreEqual(testIsLockedOut, user.IsLockedOut); + Assert.AreEqual(testLastLockoutDate, user.LastLockoutDate); + Assert.AreEqual(testLastLoginDate, user.LastLoginDate); + Assert.AreEqual(testLastPasswordChangeDate, user.LastPasswordChangeDate); + Assert.AreEqual(testComments, user.Comments); + Assert.AreEqual(testSessionTimeout, user.SessionTimeout); + Assert.AreEqual(testStartContentIds, user.StartContentIds); + Assert.AreEqual(testStartMediaIds, user.StartMediaIds); + } + } +} diff --git a/src/Umbraco.Tests/Models/UserTests.cs b/src/Umbraco.Tests/Models/UserTests.cs deleted file mode 100644 index 0bd6b17cf1..0000000000 --- a/src/Umbraco.Tests/Models/UserTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using Newtonsoft.Json; -using NUnit.Framework; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Serialization; -using Umbraco.Tests.TestHelpers; - -namespace Umbraco.Tests.Models -{ - [TestFixture] - public class UserTests - { - private IGlobalSettings GlobalSettings { get; } = SettingsForTests.DefaultGlobalSettings; - - [Test] - public void Can_Deep_Clone() - { - var item = new User(GlobalSettings) - { - Id = 3, - Key = Guid.NewGuid(), - UpdateDate = DateTime.Now, - CreateDate = DateTime.Now, - Name = "Test", - Comments = "comments", - Email = "test@test.com", - Language = "en", - FailedPasswordAttempts = 3, - IsApproved = true, - IsLockedOut = true, - LastLockoutDate = DateTime.Now, - LastLoginDate = DateTime.Now, - LastPasswordChangeDate = DateTime.Now, - //Password = "test pass", - SessionTimeout = 5, - StartContentIds = new[] { 3 }, - StartMediaIds = new[] { 8 }, - Username = "username" - }; - - var clone = (User)item.DeepClone(); - - Assert.AreNotSame(clone, item); - Assert.AreEqual(clone, item); - - Assert.AreEqual(clone.AllowedSections.Count(), item.AllowedSections.Count()); - - //Verify normal properties with reflection - var allProps = clone.GetType().GetProperties(); - foreach (var propertyInfo in allProps) - { - Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); - } - } - - [Test] - public void Can_Serialize_Without_Error() - { - var item = new User(GlobalSettings) - { - Id = 3, - Key = Guid.NewGuid(), - UpdateDate = DateTime.Now, - CreateDate = DateTime.Now, - Name = "Test", - Comments = "comments", - Email = "test@test.com", - Language = "en", - FailedPasswordAttempts = 3, - IsApproved = true, - IsLockedOut = true, - LastLockoutDate = DateTime.Now, - LastLoginDate = DateTime.Now, - LastPasswordChangeDate = DateTime.Now, - //Password = "test pass", - SessionTimeout = 5, - StartContentIds = new[] { 3 }, - StartMediaIds = new[] { 8 }, - Username = "username" - }; - - var json = JsonConvert.SerializeObject(item); - Debug.Print(json); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 8271b32a76..3a36f59776 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -297,7 +297,6 @@ - From 4456a784b0d4fa55e509b09259523d1194f1dcd8 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 11 Apr 2020 16:41:28 +0200 Subject: [PATCH 21/74] Migrated user extension tests new project pattern. --- .../Umbraco.Infrastructure.csproj | 5 ++++- .../Models/UserExtensionsTests.cs | 13 ++++++++----- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - 3 files changed, 12 insertions(+), 7 deletions(-) rename src/{Umbraco.Tests => Umbraco.Tests.UnitTests/Umbraco.Infrastructure}/Models/UserExtensionsTests.cs (93%) diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index a3f8953ac3..db3ae1bc25 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -78,7 +78,10 @@ <_Parameter1>Umbraco.Tests.Common - + + + <_Parameter1>Umbraco.Tests.UnitTests + diff --git a/src/Umbraco.Tests/Models/UserExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs similarity index 93% rename from src/Umbraco.Tests/Models/UserExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs index bf76031b59..979d18c5a1 100644 --- a/src/Umbraco.Tests/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs @@ -5,14 +5,16 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Tests.Common.Builders; -namespace Umbraco.Tests.Models +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models { [TestFixture] public class UserExtensionsTests { + private readonly UserBuilder _userBuilder = new UserBuilder(); + [TestCase(-1, "-1", "-1,1,2,3,4,5", true)] // below root start node [TestCase(2, "-1,1,2", "-1,1,2,3,4,5", true)] // below start node [TestCase(5, "-1,1,2,3,4,5", "-1,1,2,3,4,5", true)] // at start node @@ -24,9 +26,10 @@ namespace Umbraco.Tests.Models public void Determines_Path_Based_Access_To_Content(int startNodeId, string startNodePath, string contentPath, bool outcome) { - var userMock = new Mock(); - userMock.Setup(u => u.StartContentIds).Returns(new[] { startNodeId }); - var user = userMock.Object; + var user = _userBuilder + .WithStartContentIds(new[] { startNodeId }) + .Build(); + var content = Mock.Of(c => c.Path == contentPath && c.Id == 5); var esmock = new Mock(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 3a36f59776..36550066db 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -324,7 +324,6 @@ - From 3085ce52f9c531cc0a49c00ed34b50f258203a67 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 11 Apr 2020 17:21:27 +0200 Subject: [PATCH 22/74] Migrated parth validation tests to new project and builder pattern. --- .../Builders/EntitySlimBuilder.cs | 50 ++++++++++++ .../Models/PathValidationTests.cs | 76 +++++++++---------- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - 3 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs rename src/{Umbraco.Tests => Umbraco.Tests.UnitTests/Umbraco.Infrastructure}/Models/PathValidationTests.cs (71%) diff --git a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs new file mode 100644 index 0000000000..eedef3482c --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs @@ -0,0 +1,50 @@ +using Umbraco.Core.Models.Entities; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class EntitySlimBuilder + : BuilderBase, + IWithIdBuilder, + IWithParentIdBuilder + { + private int? _id; + private int? _parentId; + + public override EntitySlim Build() + { + var id = _id ?? 1; + var parentId = _parentId ?? -1; + + return new EntitySlim + { + Id = id, + ParentId = parentId, + }; + } + + public EntitySlimBuilder WithNoId() + { + _id = 0; + return this; + } + + public EntitySlimBuilder WithNoParentId() + { + _parentId = 0; + return this; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + int? IWithParentIdBuilder.ParentId + { + get => _parentId; + set => _parentId = value; + } + } +} diff --git a/src/Umbraco.Tests/Models/PathValidationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs similarity index 71% rename from src/Umbraco.Tests/Models/PathValidationTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs index dab362cc35..36a282aaf4 100644 --- a/src/Umbraco.Tests/Models/PathValidationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs @@ -4,16 +4,22 @@ using NUnit.Framework; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Common.Builders.Extensions; -namespace Umbraco.Tests.Models +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models { [TestFixture] public class PathValidationTests { + private readonly EntitySlimBuilder _builder = new EntitySlimBuilder(); + [Test] public void Validate_Path() { - var entity = new EntitySlim(); + var entity = _builder + .WithNoId() + .Build(); //it's empty with no id so we need to allow it Assert.IsTrue(entity.ValidatePath()); @@ -37,7 +43,9 @@ namespace Umbraco.Tests.Models [Test] public void Ensure_Path_Throws_Without_Id() { - var entity = new EntitySlim(); + var entity = _builder + .WithNoId() + .Build(); //no id assigned Assert.Throws(() => entity.EnsureValidPath(Mock.Of(), umbracoEntity => new EntitySlim(), umbracoEntity => { })); @@ -46,7 +54,10 @@ namespace Umbraco.Tests.Models [Test] public void Ensure_Path_Throws_Without_Parent() { - var entity = new EntitySlim { Id = 1234 }; + var entity = _builder + .WithId(1234) + .WithNoParentId() + .Build(); //no parent found Assert.Throws(() => entity.EnsureValidPath(Mock.Of(), umbracoEntity => null, umbracoEntity => { })); @@ -55,12 +66,9 @@ namespace Umbraco.Tests.Models [Test] public void Ensure_Path_Entity_At_Root() { - var entity = new EntitySlim - { - Id = 1234, - ParentId = -1 - }; - + var entity = _builder + .WithId(1234) + .Build(); entity.EnsureValidPath(Mock.Of(), umbracoEntity => null, umbracoEntity => { }); @@ -71,11 +79,10 @@ namespace Umbraco.Tests.Models [Test] public void Ensure_Path_Entity_Valid_Parent() { - var entity = new EntitySlim - { - Id = 1234, - ParentId = 888 - }; + var entity = _builder + .WithId(1234) + .WithParentId(888) + .Build(); entity.EnsureValidPath(Mock.Of(), umbracoEntity => umbracoEntity.ParentId == 888 ? new EntitySlim { Id = 888, Path = "-1,888" } : null, umbracoEntity => { }); @@ -86,29 +93,22 @@ namespace Umbraco.Tests.Models [Test] public void Ensure_Path_Entity_Valid_Recursive_Parent() { - var parentA = new EntitySlim - { - Id = 999, - ParentId = -1 - }; + var parentA = _builder + .WithId(999) + .Build(); + var parentB = _builder + .WithId(888) + .WithParentId(999) + .Build(); + var parentC = _builder + .WithId(777) + .WithParentId(888) + .Build(); - var parentB = new EntitySlim - { - Id = 888, - ParentId = 999 - }; - - var parentC = new EntitySlim - { - Id = 777, - ParentId = 888 - }; - - var entity = new EntitySlim - { - Id = 1234, - ParentId = 777 - }; + var entity = _builder + .WithId(1234) + .WithParentId(777) + .Build(); Func getParent = umbracoEntity => { @@ -136,4 +136,4 @@ namespace Umbraco.Tests.Models Assert.AreEqual("-1,999,888,777,1234", entity.Path); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 36550066db..9c2e598875 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -142,7 +142,6 @@ - From a5aa1e19a5b8a734d3e8255e1157d54787689abf Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 11 Apr 2020 18:37:10 +0200 Subject: [PATCH 23/74] Fixed failing integration tests following amends to UserBuilder. --- .../Builders/EntitySlimBuilder.cs | 6 ------ .../Builders/Extensions/BuilderExtensions.cs | 7 +++++++ .../Repositories/UserRepositoryTest.cs | 20 +++++++++---------- .../Testing/UmbracoIntegrationTest.cs | 4 ---- .../Models/PathValidationTests.cs | 4 ++-- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs index eedef3482c..844c765a9d 100644 --- a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs @@ -23,12 +23,6 @@ namespace Umbraco.Tests.Common.Builders }; } - public EntitySlimBuilder WithNoId() - { - _id = 0; - return this; - } - public EntitySlimBuilder WithNoParentId() { _parentId = 0; diff --git a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs index 3228181edb..f3c0b9796e 100644 --- a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs @@ -12,6 +12,13 @@ namespace Umbraco.Tests.Common.Builders.Extensions return builder; } + public static T WithoutIdentity(this T builder) + where T : IWithIdBuilder + { + builder.Id = 0; + return builder; + } + public static T WithCreatorId(this T builder, int creatorId) where T : IWithCreatorIdBuilder { diff --git a/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs index 7feb69f0c6..a590121556 100644 --- a/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs @@ -1,21 +1,19 @@ -using System.Linq; +using System; +using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Logging; +using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; -using Umbraco.Tests.Testing; -using Umbraco.Core.Persistence; -using Umbraco.Core.PropertyEditors; -using System; -using Umbraco.Core.Configuration; -using Umbraco.Core.Services.Implement; +using Umbraco.Tests.Common.Builders.Extensions; using Umbraco.Tests.Integration.Testing; +using Umbraco.Tests.Testing; namespace Umbraco.Tests.Persistence.Repositories { @@ -381,9 +379,9 @@ namespace Umbraco.Tests.Persistence.Repositories private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository) { - var user1 = UserBuilder.WithSuffix("1").Build(); - var user2 = UserBuilder.WithSuffix("2").Build(); - var user3 = UserBuilder.WithSuffix("3").Build(); + var user1 = UserBuilder.WithoutIdentity().WithSuffix("1").Build(); + var user2 = UserBuilder.WithoutIdentity().WithSuffix("2").Build(); + var user3 = UserBuilder.WithoutIdentity().WithSuffix("3").Build(); repository.Save(user1); repository.Save(user2); repository.Save(user3); diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 0f3895cfbf..101feb79a4 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using NPoco.Expressions; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Composing; @@ -21,7 +18,6 @@ using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Umbraco.Web.BackOffice.AspNetCore; -using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Extensions; namespace Umbraco.Tests.Integration.Testing diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs index 36a282aaf4..92f728d4b8 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models public void Validate_Path() { var entity = _builder - .WithNoId() + .WithoutIdentity() .Build(); //it's empty with no id so we need to allow it @@ -44,7 +44,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models public void Ensure_Path_Throws_Without_Id() { var entity = _builder - .WithNoId() + .WithoutIdentity() .Build(); //no id assigned From f961916245dc1ea9e58983bf8403750f7ac4811a Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sun, 12 Apr 2020 09:47:44 +0200 Subject: [PATCH 24/74] Introduced a reset of the unit test builders to support re-use within a test. --- .../Builders/BuilderBase.cs | 2 + .../Builders/ConfigurationEditorBuilder.cs | 7 +- .../Builders/DataEditorBuilder.cs | 12 +- .../Builders/DataTypeBuilder.cs | 29 +++-- .../Builders/DataValueEditorBuilder.cs | 10 +- .../Builders/DictionaryItemBuilder.cs | 12 ++ .../Builders/DictionaryTranslationBuilder.cs | 75 +++++++------ .../Builders/EntitySlimBuilder.cs | 7 ++ .../Builders/GenericCollectionBuilder.cs | 18 ++- .../Builders/GenericDictionaryBuilder.cs | 20 +++- .../Builders/GlobalSettingsBuilder.cs | 31 ++++- .../Builders/LanguageBuilder.cs | 102 +++++++++-------- .../Builders/MemberBuilder.cs | 25 +++++ .../Builders/MemberGroupBuilder.cs | 11 ++ .../Builders/MemberTypeBuilder.cs | 16 +++ .../Builders/PropertyBuilder.cs | 9 ++ .../Builders/PropertyGroupBuilder.cs | 12 ++ .../Builders/PropertyTypeBuilder.cs | 21 ++++ .../Builders/RelationBuilder.cs | 12 ++ .../Builders/RelationTypeBuilder.cs | 97 +++++++++------- .../Builders/SmtpSettingsBuilder.cs | 106 ++++++++++-------- .../Builders/StylesheetBuilder.cs | 7 ++ .../Builders/TemplateBuilder.cs | 17 +++ .../Builders/UserBuilder.cs | 27 ++++- .../Builders/UserGroupBuilder.cs | 18 ++- .../Repositories/UserRepositoryTest.cs | 5 +- 26 files changed, 513 insertions(+), 195 deletions(-) diff --git a/src/Umbraco.Tests.Common/Builders/BuilderBase.cs b/src/Umbraco.Tests.Common/Builders/BuilderBase.cs index d8fc048d1b..723581986c 100644 --- a/src/Umbraco.Tests.Common/Builders/BuilderBase.cs +++ b/src/Umbraco.Tests.Common/Builders/BuilderBase.cs @@ -3,5 +3,7 @@ namespace Umbraco.Tests.Common.Builders public abstract class BuilderBase { public abstract T Build(); + + protected abstract void Reset(); } } diff --git a/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs b/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs index 3e0b9b96a9..08cb9e07f5 100644 --- a/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs @@ -7,12 +7,10 @@ namespace Umbraco.Tests.Common.Builders { private IDictionary _defaultConfiguration; - public ConfigurationEditorBuilder(TParent parentBuilder) : base(parentBuilder) { } - public ConfigurationEditorBuilder WithDefaultConfiguration(IDictionary defaultConfiguration) { _defaultConfiguration = defaultConfiguration; @@ -23,11 +21,16 @@ namespace Umbraco.Tests.Common.Builders { var defaultConfiguration = _defaultConfiguration ?? new Dictionary(); + Reset(); return new ConfigurationEditor() { DefaultConfiguration = defaultConfiguration, }; } + protected override void Reset() + { + _defaultConfiguration = null; + } } } diff --git a/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs b/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs index b3c3ff1b7e..b1e33f53df 100644 --- a/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs @@ -9,8 +9,8 @@ namespace Umbraco.Tests.Common.Builders { public class DataEditorBuilder : ChildBuilderBase { - private readonly ConfigurationEditorBuilder> _explicitConfigurationEditorBuilder; - private readonly DataValueEditorBuilder> _explicitValueEditorBuilder; + private ConfigurationEditorBuilder> _explicitConfigurationEditorBuilder; + private DataValueEditorBuilder> _explicitValueEditorBuilder; private IDictionary _defaultConfiguration; public DataEditorBuilder(TParent parentBuilder) : base(parentBuilder) @@ -37,6 +37,7 @@ namespace Umbraco.Tests.Common.Builders var explicitConfigurationEditor = _explicitConfigurationEditorBuilder.Build(); var explicitValueEditor = _explicitValueEditorBuilder.Build(); + Reset(); return new DataEditor( Mock.Of(), Mock.Of(), @@ -50,5 +51,12 @@ namespace Umbraco.Tests.Common.Builders ExplicitValueEditor = explicitValueEditor }; } + + protected override void Reset() + { + _defaultConfiguration = null; + _explicitConfigurationEditorBuilder = new ConfigurationEditorBuilder>(this); + _explicitValueEditorBuilder = new DataValueEditorBuilder>(this); + } } } diff --git a/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs index de7b8d9a97..582c7f58b5 100644 --- a/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests.Common.Builders IWithPathBuilder, IWithSortOrderBuilder { - private readonly DataEditorBuilder _dataEditorBuilder; + private DataEditorBuilder _dataEditorBuilder; private int? _id; private int? _parentId; private Guid? _key; @@ -28,7 +28,6 @@ namespace Umbraco.Tests.Common.Builders private DateTime? _deleteDate; private string _name; private bool? _trashed; - // private object _configuration; private int? _level; private string _path; private int? _creatorId; @@ -40,12 +39,6 @@ namespace Umbraco.Tests.Common.Builders _dataEditorBuilder = new DataEditorBuilder(this); } - // public DataTypeBuilder WithConfiguration(object configuration) - // { - // _configuration = configuration; - // return this; - // } - public DataTypeBuilder WithDatabaseType(ValueStorageType databaseType) { _databaseType = databaseType; @@ -67,13 +60,13 @@ namespace Umbraco.Tests.Common.Builders var updateDate = _updateDate ?? DateTime.Now; var deleteDate = _deleteDate ?? null; var name = _name ?? Guid.NewGuid().ToString(); - // var configuration = _configuration ?? editor.GetConfigurationEditor().DefaultConfigurationObject; var level = _level ?? 0; var path = _path ?? string.Empty; var creatorId = _creatorId ?? 1; var databaseType = _databaseType ?? ValueStorageType.Ntext; var sortOrder = _sortOrder ?? 0; + Reset(); return new DataType(editor, parentId) { Id = id, @@ -91,6 +84,24 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _dataEditorBuilder = new DataEditorBuilder(this); + _id = null; + _parentId = null; + _key = null; + _createDate = null; + _updateDate = null; + _deleteDate = null; + _name = null; + _trashed = null; + _level = null; + _path = null; + _creatorId = null; + _databaseType = null; + _sortOrder = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs b/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs index 3d0b518ee7..4c271d127c 100644 --- a/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs @@ -13,7 +13,6 @@ namespace Umbraco.Tests.Common.Builders private bool? _hideLabel; private string _valueType; - public DataValueEditorBuilder(TParent parentBuilder) : base(parentBuilder) { } @@ -49,6 +48,7 @@ namespace Umbraco.Tests.Common.Builders var hideLabel = _hideLabel ?? false; var valueType = _valueType ?? Guid.NewGuid().ToString(); + Reset(); return new DataValueEditor( Mock.Of(), Mock.Of(), @@ -62,5 +62,13 @@ namespace Umbraco.Tests.Common.Builders ValueType = valueType, }; } + + protected override void Reset() + { + _configuration = null; + _view = null; + _hideLabel = null; + _valueType = null; + } } } diff --git a/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs index 206bccba80..17ccbcc3fb 100644 --- a/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs @@ -65,6 +65,7 @@ namespace Umbraco.Tests.Common.Builders var parentId = _parentId ?? null; var itemKey = _itemKey ?? Guid.NewGuid().ToString(); + Reset(); var result = new DictionaryItem(itemKey) { Translations = _translationBuilders.Select(x => x.Build()), @@ -78,6 +79,17 @@ namespace Umbraco.Tests.Common.Builders return result; } + protected override void Reset() + { + _createDate = null; + _deleteDate = null; + _id = null; + _itemKey = null; + _key = null; + _parentId = null; + _updateDate = null; + } + public DictionaryItemBuilder WithParentId(Guid parentId) { _parentId = parentId; diff --git a/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs index d2f9d4bf02..6104b005cf 100644 --- a/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs @@ -12,8 +12,7 @@ namespace Umbraco.Tests.Common.Builders IWithDeleteDateBuilder, IWithKeyBuilder { - private readonly LanguageBuilder _languageBuilder; - private readonly Guid? _uniqueId = null; + private LanguageBuilder _languageBuilder; private DateTime? _createDate; private DateTime? _deleteDate; private int? _id; @@ -26,6 +25,48 @@ namespace Umbraco.Tests.Common.Builders _languageBuilder = new LanguageBuilder(this); } + public LanguageBuilder AddLanguage() => _languageBuilder; + + public DictionaryTranslationBuilder WithValue(string value) + { + _value = value; + return this; + } + + public override IDictionaryTranslation Build() + { + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + var id = _id ?? 1; + var key = _key ?? Guid.NewGuid(); + + var result = new DictionaryTranslation( + _languageBuilder.Build(), + _value ?? Guid.NewGuid().ToString(), + key) + { + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate, + Id = id + }; + + Reset(); + return result; + } + + protected override void Reset() + { + _languageBuilder = new LanguageBuilder(this); + _createDate = null; + _deleteDate = null; + _id = null; + _key = null; + _updateDate = null; + _value = null; + } + DateTime? IWithCreateDateBuilder.CreateDate { get => _createDate; @@ -55,35 +96,5 @@ namespace Umbraco.Tests.Common.Builders get => _updateDate; set => _updateDate = value; } - - public override IDictionaryTranslation Build() - { - var createDate = _createDate ?? DateTime.Now; - var updateDate = _updateDate ?? DateTime.Now; - var deleteDate = _deleteDate ?? null; - var id = _id ?? 1; - var key = _key ?? Guid.NewGuid(); - - var result = new DictionaryTranslation( - _languageBuilder.Build(), - _value ?? Guid.NewGuid().ToString(), - _uniqueId ?? key) - { - CreateDate = createDate, - UpdateDate = updateDate, - DeleteDate = deleteDate, - Id = id - }; - - return result; - } - - public LanguageBuilder AddLanguage() => _languageBuilder; - - public DictionaryTranslationBuilder WithValue(string value) - { - _value = value; - return this; - } } } diff --git a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs index 844c765a9d..f4649d6b49 100644 --- a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs @@ -16,6 +16,7 @@ namespace Umbraco.Tests.Common.Builders var id = _id ?? 1; var parentId = _parentId ?? -1; + Reset(); return new EntitySlim { Id = id, @@ -23,6 +24,12 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _id = null; + _parentId = null; + } + public EntitySlimBuilder WithNoParentId() { _parentId = 0; diff --git a/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs b/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs index c7e176e9b0..290fdfc45b 100644 --- a/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs @@ -1,24 +1,36 @@ using System.Collections.Generic; +using System.Linq; namespace Umbraco.Tests.Common.Builders { public class GenericCollectionBuilder : ChildBuilderBase> { - private readonly IList _collection; + private IList _collection; public GenericCollectionBuilder(TBuilder parentBuilder) : base(parentBuilder) { - _collection = new List(); } public override IEnumerable Build() { - return _collection; + var collection = _collection?.ToList() ?? Enumerable.Empty(); + Reset(); + return collection; + } + + protected override void Reset() + { + _collection = null; } public GenericCollectionBuilder WithValue(T value) { + if (_collection == null) + { + _collection = new List(); + } + _collection.Add(value); return this; } diff --git a/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs b/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs index 8f6aedcf43..444bbb61d2 100644 --- a/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs @@ -5,20 +5,34 @@ namespace Umbraco.Tests.Common.Builders public class GenericDictionaryBuilder : ChildBuilderBase> { - private readonly IDictionary _dictionary; + private IDictionary _dictionary; public GenericDictionaryBuilder(TBuilder parentBuilder) : base(parentBuilder) { - _dictionary = new Dictionary(); } public override IDictionary Build() { - return _dictionary; + var dictionary = _dictionary == null + ? new Dictionary() + : new Dictionary(_dictionary); + Reset(); + return dictionary; } + protected override void Reset() + { + _dictionary = null; + } + + public GenericDictionaryBuilder WithKeyValue(TKey key, TValue value) { + if (_dictionary == null) + { + _dictionary = new Dictionary(); + } + _dictionary.Add(key, value); return this; } diff --git a/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs b/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs index 56e242146c..1fe1164eba 100644 --- a/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs @@ -32,8 +32,7 @@ namespace Umbraco.Tests.Common.Builders private string _noNodesViewPath; private bool? _useHttps; private int? _versionCheckPeriod; - private readonly SmtpSettingsBuilder> _smtpSettingsBuilder; - + private SmtpSettingsBuilder> _smtpSettingsBuilder; public GlobalSettingsBuilder(TParent parentBuilder) : base(parentBuilder) { @@ -192,7 +191,7 @@ namespace Umbraco.Tests.Common.Builders var mainDomLock = _mainDomLock ?? string.Empty; var noNodesViewPath = _noNodesViewPath ?? "~/config/splashes/NoNodes.cshtml"; - + Reset(); return new TestGlobalSettings { ConfigurationStatus = configurationStatus, @@ -220,6 +219,32 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _configurationStatus = null; + _databaseFactoryServerVersion = null; + _defaultUiLanguage = null; + _disableElectionForSingleServer = null; + _hideTopLevelNodeFromPath = null; + _installEmptyDatabase = null; + _installMissingDatabase = null; + _isSmtpServerConfigured = null; + _path = null; + _registerType = null; + _reservedPaths = null; + _reservedUrls = null; + _timeOutInMinutes = null; + _umbracoCssPath = null; + _umbracoMediaPath = null; + _umbracoPath = null; + _umbracoScriptsPath = null; + _mainDomLock = null; + _noNodesViewPath = null; + _useHttps = null; + _versionCheckPeriod = null; + _smtpSettingsBuilder = new SmtpSettingsBuilder>(this); + } + private class TestGlobalSettings : IGlobalSettings { public string ReservedUrls { get; set; } diff --git a/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs index ae60920c9c..186b961526 100644 --- a/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs @@ -37,6 +37,64 @@ namespace Umbraco.Tests.Common.Builders { } + public LanguageBuilder WithIsDefault(bool isDefault) + { + _isDefault = isDefault; + return this; + } + + public LanguageBuilder WithIsMandatory(bool isMandatory) + { + _isMandatory = isMandatory; + return this; + } + + public LanguageBuilder WithFallbackLanguageId(int fallbackLanguageId) + { + _fallbackLanguageId = fallbackLanguageId; + return this; + } + + public override ILanguage Build() + { + var cultureInfo = _cultureInfo ?? CultureInfo.GetCultureInfo("en-US"); + var key = _key ?? Guid.NewGuid(); + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + var fallbackLanguageId = _fallbackLanguageId ?? null; + var isDefault = _isDefault ?? false; + var isMandatory = _isMandatory ?? false; + + Reset(); + return new Language(Mock.Of(), cultureInfo.Name) + { + Id = _id ?? 1, + CultureName = cultureInfo.TwoLetterISOLanguageName, + IsoCode = new RegionInfo(cultureInfo.LCID).Name, + Key = key, + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate, + IsDefault = isDefault, + IsMandatory = isMandatory, + FallbackLanguageId = fallbackLanguageId + }; + } + + protected override void Reset() + { + _createDate = null; + _cultureInfo = null; + _deleteDate = null; + _fallbackLanguageId = null; + _id = null; + _isDefault = null; + _isMandatory = null; + _key = null; + _updateDate = null; + } + DateTime? IWithCreateDateBuilder.CreateDate { get => _createDate; @@ -72,49 +130,5 @@ namespace Umbraco.Tests.Common.Builders get => _updateDate; set => _updateDate = value; } - - public override ILanguage Build() - { - var cultureInfo = _cultureInfo ?? CultureInfo.GetCultureInfo("en-US"); - var key = _key ?? Guid.NewGuid(); - var createDate = _createDate ?? DateTime.Now; - var updateDate = _updateDate ?? DateTime.Now; - var deleteDate = _deleteDate ?? null; - var fallbackLanguageId = _fallbackLanguageId ?? null; - var isDefault = _isDefault ?? false; - var isMandatory = _isMandatory ?? false; - - return new Language(Mock.Of(), cultureInfo.Name) - { - Id = _id ?? 1, - CultureName = cultureInfo.TwoLetterISOLanguageName, - IsoCode = new RegionInfo(cultureInfo.LCID).Name, - Key = key, - CreateDate = createDate, - UpdateDate = updateDate, - DeleteDate = deleteDate, - IsDefault = isDefault, - IsMandatory = isMandatory, - FallbackLanguageId = fallbackLanguageId - }; - } - - public LanguageBuilder WithIsDefault(bool isDefault) - { - _isDefault = isDefault; - return this; - } - - public LanguageBuilder WithIsMandatory(bool isMandatory) - { - _isMandatory = isMandatory; - return this; - } - - public LanguageBuilder WithFallbackLanguageId(int fallbackLanguageId) - { - _fallbackLanguageId = fallbackLanguageId; - return this; - } } } diff --git a/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs index 16ec7ed005..f32035c5c9 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs @@ -161,9 +161,34 @@ namespace Umbraco.Tests.Common.Builders member.ResetDirtyProperties(false); } + Reset(); return member; } + protected override void Reset() + { + _id = null; + _key = null; + _createDate = null; + _updateDate = null; + _name = null; + _creatorId = null; + _level = null; + _path = null; + _username = null; + _rawPasswordValue = null; + _email = null; + _failedPasswordAttempts = null; + _isApproved = null; + _isLockedOut = null; + _lastLockoutDate = null; + _lastLoginDate = null; + _lastPasswordChangeDate = null; + _sortOrder = null; + _trashed = null; + _propertyIdsIncrementingFrom = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs index bfd7f30a14..127a071083 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs @@ -57,9 +57,20 @@ namespace Umbraco.Tests.Common.Builders } } + Reset(); return memberGroup; } + protected override void Reset() + { + _id = null; + _key = null; + _createDate = null; + _updateDate = null; + _name = null; + _creatorId = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs index d5a145331e..c132163c21 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs @@ -134,9 +134,25 @@ namespace Umbraco.Tests.Common.Builders memberType.ResetDirtyProperties(false); + Reset(); return memberType; } + protected override void Reset() + { + _id = null; + _alias = null; + _name = null; + _parentId = null; + _sortOrder = null; + _creatorId = null; + _description = null; + _icon = null; + _thumbnail = null; + _trashed = null; + _propertyGroupBuilders.Clear(); + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs b/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs index 6cb7a431f2..6396b604e9 100644 --- a/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs @@ -36,6 +36,7 @@ namespace Umbraco.Tests.Common.Builders // Needs to be within collection to support publishing. var propertyTypeCollection = new PropertyTypeCollection(true, new[] { _propertyTypeBuilder.Build() }); + Reset(); return new Property(id, propertyTypeCollection[0]) { Key = key, @@ -44,6 +45,14 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _id = null; + _key = null; + _createDate = null; + _updateDate = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs index 5df61dd072..25ec8bf21a 100644 --- a/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs @@ -62,6 +62,7 @@ namespace Umbraco.Tests.Common.Builders properties.Add(propertyType); } + Reset(); return new PropertyGroup(properties) { Id = id, @@ -73,6 +74,17 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _id = null; + _key = null; + _createDate = null; + _updateDate = null; + _name = null; + _sortOrder = null; + _propertyTypeBuilders.Clear(); + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs index 9dc61014b5..3c499fb855 100644 --- a/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs @@ -108,6 +108,7 @@ namespace Umbraco.Tests.Common.Builders var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); + Reset(); return new PropertyType(shortStringHelper, propertyEditorAlias, valueStorageType) { Id = id, @@ -126,6 +127,26 @@ namespace Umbraco.Tests.Common.Builders ValidationRegExpMessage = validationRegExpMessage, }; } + + protected override void Reset() + { + _id = null; + _key = null; + _propertyEditorAlias = null; + _valueStorageType = null; + _alias = null; + _name = null; + _createDate = null; + _updateDate = null; + _sortOrder = null; + _description = null; + _dataTypeId = null; + _propertyGroupId = null; + _mandatory = null; + _mandatoryMessage = null; + _validationRegExp = null; + _validationRegExpMessage = null; + } int? IWithIdBuilder.Id { diff --git a/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs b/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs index 2cea234200..bf805aba77 100644 --- a/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs @@ -58,6 +58,7 @@ namespace Umbraco.Tests.Common.Builders var relationType = _relationTypeBuilder.Build(); + Reset(); return new Relation(parentId, childId, relationType) { Comment = comment, @@ -68,6 +69,17 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _id = null; + _parentId = null; + _childId = null; + _key = null; + _createDate = null; + _updateDate = null; + _comment = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs index 677d02bf09..dd25ae864a 100644 --- a/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs @@ -33,6 +33,62 @@ namespace Umbraco.Tests.Common.Builders { } + public RelationTypeBuilder WithIsBidirectional(bool isBidirectional) + { + _isBidirectional = isBidirectional; + return this; + } + + public RelationTypeBuilder WithChildObjectType(Guid childObjectType) + { + _childObjectType = childObjectType; + return this; + } + + public RelationTypeBuilder WithParentObjectType(Guid parentObjectType) + { + _parentObjectType = parentObjectType; + return this; + } + + public override IRelationType Build() + { + var alias = _alias ?? Guid.NewGuid().ToString(); + var name = _name ?? Guid.NewGuid().ToString(); + var parentObjectType = _parentObjectType ?? null; + var childObjectType = _childObjectType ?? null; + var id = _id ?? 1; + var key = _key ?? Guid.NewGuid(); + var isBidirectional = _isBidirectional ?? false; + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + + Reset(); + return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType) + { + Id = id, + Key = key, + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate + }; + } + + protected override void Reset() + { + _alias = null; + _childObjectType = null; + _createDate = null; + _deleteDate = null; + _id = null; + _isBidirectional = null; + _key = null; + _name = null; + _parentObjectType = null; + _updateDate = null; + } + string IWithAliasBuilder.Alias { get => _alias; @@ -74,46 +130,5 @@ namespace Umbraco.Tests.Common.Builders get => _updateDate; set => _updateDate = value; } - - public override IRelationType Build() - { - var alias = _alias ?? Guid.NewGuid().ToString(); - var name = _name ?? Guid.NewGuid().ToString(); - var parentObjectType = _parentObjectType ?? null; - var childObjectType = _childObjectType ?? null; - var id = _id ?? 1; - var key = _key ?? Guid.NewGuid(); - var isBidirectional = _isBidirectional ?? false; - var createDate = _createDate ?? DateTime.Now; - var updateDate = _updateDate ?? DateTime.Now; - var deleteDate = _deleteDate ?? null; - - return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType) - { - Id = id, - Key = key, - CreateDate = createDate, - UpdateDate = updateDate, - DeleteDate = deleteDate - }; - } - - public RelationTypeBuilder WithIsBidirectional(bool isBidirectional) - { - _isBidirectional = isBidirectional; - return this; - } - - public RelationTypeBuilder WithChildObjectType(Guid childObjectType) - { - _childObjectType = childObjectType; - return this; - } - - public RelationTypeBuilder WithParentObjectType(Guid parentObjectType) - { - _parentObjectType = parentObjectType; - return this; - } } } diff --git a/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs b/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs index 3120cc95f6..586aedec9e 100644 --- a/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs @@ -9,65 +9,73 @@ namespace Umbraco.Tests.Common.Builders } } - public class SmtpSettingsBuilder - : ChildBuilderBase - { - private string _from; - private string _host; - private int? _port; - private string _pickupDirectoryLocation; + public class SmtpSettingsBuilder + : ChildBuilderBase + { + private string _from; + private string _host; + private int? _port; + private string _pickupDirectoryLocation; - public SmtpSettingsBuilder(TParent parentBuilder) : base(parentBuilder) - { - } + public SmtpSettingsBuilder(TParent parentBuilder) : base(parentBuilder) + { + } - public SmtpSettingsBuilder WithFrom(string from) - { - _from = from; - return this; - } + public SmtpSettingsBuilder WithFrom(string from) + { + _from = from; + return this; + } - public SmtpSettingsBuilder WithHost(string host) - { - _host = host; - return this; - } + public SmtpSettingsBuilder WithHost(string host) + { + _host = host; + return this; + } - public SmtpSettingsBuilder WithPost(int port) - { - _port = port; - return this; - } + public SmtpSettingsBuilder WithPost(int port) + { + _port = port; + return this; + } - public SmtpSettingsBuilder WithPickupDirectoryLocation(string pickupDirectoryLocation) - { - _pickupDirectoryLocation = pickupDirectoryLocation; - return this; - } + public SmtpSettingsBuilder WithPickupDirectoryLocation(string pickupDirectoryLocation) + { + _pickupDirectoryLocation = pickupDirectoryLocation; + return this; + } + public override ISmtpSettings Build() + { + var from = _from ?? null; + var host = _host ?? null; + var port = _port ?? 25; + var pickupDirectoryLocation = _pickupDirectoryLocation ?? null; - public override ISmtpSettings Build() - { - var from = _from ?? null; - var host = _host ?? null; - var port = _port ?? 25; - var pickupDirectoryLocation = _pickupDirectoryLocation ?? null; - - return new TestSmtpSettings() - { + Reset(); + return new TestSmtpSettings() + { From = from, Host = host, Port = port, PickupDirectoryLocation = pickupDirectoryLocation, - }; - } + }; + } - private class TestSmtpSettings : ISmtpSettings - { - public string From { get; set; } - public string Host { get; set; } - public int Port { get; set; } - public string PickupDirectoryLocation { get; set; } - } - } + protected override void Reset() + { + _from = null; + _host = null; + _port = null; + _pickupDirectoryLocation = null; + } + + private class TestSmtpSettings : ISmtpSettings + { + public string From { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public string PickupDirectoryLocation { get; set; } + } + } } diff --git a/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs b/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs index ed871b9c31..c3eb097e65 100644 --- a/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs @@ -25,10 +25,17 @@ namespace Umbraco.Tests.Common.Builders var path = _path ?? string.Empty; var content = _content ?? string.Empty; + Reset(); return new Stylesheet(path) { Content = content, }; } + + protected override void Reset() + { + _path = null; + _content = null; + } } } diff --git a/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs b/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs index 7f7fa0f098..6a29b89e6a 100644 --- a/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs @@ -57,6 +57,8 @@ namespace Umbraco.Tests.Common.Builders var masterTemplateId = _masterTemplateId ?? null; var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); + + Reset(); return new Template(shortStringHelper, name, alias) { Id = id, @@ -71,6 +73,21 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _id = null; + _key = null; + _alias = null; + _name = null; + _createDate = null; + _updateDate = null; + _path = null; + _content = null; + _isMasterTemplate = null; + _masterTemplateAlias = null; + _masterTemplateId = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs index 4fafb3e1bd..4d69280ec5 100644 --- a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -10,7 +10,6 @@ namespace Umbraco.Tests.Common.Builders { } } - public class UserBuilder : ChildBuilderBase, IWithIdBuilder, @@ -135,6 +134,7 @@ namespace Umbraco.Tests.Common.Builders var startContentIds = _startContentIds ?? new int[0]; var startMediaIds = _startMediaIds ?? new int[0]; + Reset(); return new User( globalSettings, name, @@ -160,6 +160,31 @@ namespace Umbraco.Tests.Common.Builders }; } + protected override void Reset() + { + _id = null; + _key = null; + _createDate = null; + _updateDate = null; + _language = null; + _name = null; + _username = null; + _rawPasswordValue = null; + _email = null; + _failedPasswordAttempts = null; + _isApproved = null; + _isLockedOut = null; + _lastLockoutDate = null; + _lastLoginDate = null; + _lastPasswordChangeDate = null; + _suffix = string.Empty; + _defaultLang = null; + _comments = null; + _sessionTimeout = null; + _startContentIds = null; + _startMediaIds = null; + } + int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs index 4c6bb7a74c..8b0568d2d4 100644 --- a/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -6,7 +6,6 @@ using Umbraco.Tests.Common.Builders.Interfaces; namespace Umbraco.Tests.Common.Builders { - public class UserGroupBuilder : UserGroupBuilder { public UserGroupBuilder() : base(null) @@ -61,7 +60,7 @@ namespace Umbraco.Tests.Common.Builders public override IUserGroup Build() { - return Mock.Of(x => + var userGroup = Mock.Of(x => x.StartContentId == _startContentId && x.StartMediaId == _startMediaId && x.Name == (_name ?? ("TestUserGroup" + _suffix)) && @@ -69,6 +68,21 @@ namespace Umbraco.Tests.Common.Builders x.Icon == _icon && x.Permissions == _permissions && x.AllowedSections == _sectionCollection); + Reset(); + return userGroup; + } + + protected override void Reset() + { + _startContentId = null; + _startMediaId = null; + _alias = null; + _icon = null; + _name = null; + _permissions = Enumerable.Empty(); + _sectionCollection = Enumerable.Empty(); + _suffix = null; + _id = null; } int? IWithIdBuilder.Id diff --git a/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs index a590121556..23c9e398d3 100644 --- a/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs @@ -87,7 +87,7 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var user = UserBuilder.Build(); + var user = UserBuilder.WithoutIdentity().Build(); repository.Save(user); @@ -365,10 +365,9 @@ namespace Umbraco.Tests.Persistence.Repositories private User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository) { - var user = UserBuilder.Build(); + var user = UserBuilder.WithoutIdentity().Build(); repository.Save(user); - var group = UserGroupBuilder.Build(); userGroupRepository.AddOrUpdateGroupWithUsers(@group, new[] { user.Id }); From a9a8cb7f6f4fd8a77afc9c6f2afc5f23fed4dedf Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 13 Apr 2020 19:22:31 +0200 Subject: [PATCH 25/74] Migrated macro unit tests to new project and builder pattern. --- .../Builders/MacroBuilder.cs | 141 ++++++++++++++++++ .../Builders/MacroPropertyBuilder.cs | 88 +++++++++++ .../Models/MacroTests.cs | 32 ++-- .../Builders/MacroBuilderTests.cs | 73 +++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - 5 files changed, 325 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Tests.Common/Builders/MacroBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs rename src/{Umbraco.Tests => Umbraco.Tests.UnitTests/Umbraco.Infrastructure}/Models/MacroTests.cs (65%) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MacroBuilderTests.cs diff --git a/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs b/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs new file mode 100644 index 0000000000..953f7c9a03 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; +using Umbraco.Core.Strings; +using Umbraco.Tests.Common.Builders.Extensions; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class MacroBuilder + : BuilderBase, + IWithIdBuilder, + IWithKeyBuilder, + IWithAliasBuilder, + IWithNameBuilder + { + private List _propertyBuilders = new List(); + + private int? _id; + private Guid? _key; + private string _alias; + private string _name; + private bool? _useInEditor; + private int? _cacheDuration; + private bool? _cacheByPage; + private bool? _cacheByMember; + private bool? _dontRender; + private string _macroSource; + + public MacroBuilder WithUseInEditor(bool useInEditor) + { + _useInEditor = useInEditor; + return this; + } + + public MacroBuilder WithCacheDuration(int cacheDuration) + { + _cacheDuration = cacheDuration; + return this; + } + + public MacroBuilder WithCacheByPage(bool cacheByPage) + { + _cacheByPage = cacheByPage; + return this; + } + + public MacroBuilder WithCacheByMember(bool cacheByMember) + { + _cacheByMember = cacheByMember; + return this; + } + + public MacroBuilder WithDontRender(bool dontRender) + { + _dontRender = dontRender; + return this; + } + + public MacroBuilder WithSource(string macroSource) + { + _macroSource = macroSource; + return this; + } + + public MacroPropertyBuilder AddProperty() + { + var builder = new MacroPropertyBuilder(this); + + _propertyBuilders.Add(builder); + + return builder; + } + + public override Macro Build() + { + var id = _id ?? 1; + var name = _name ?? Guid.NewGuid().ToString(); + var alias = _alias ?? name.ToCamelCase(); + var key = _key ?? Guid.NewGuid(); + var useInEditor = _useInEditor ?? false; + var cacheDuration = _cacheDuration ?? 0; + var cacheByPage = _cacheByPage ?? false; + var cacheByMember = _cacheByMember ?? false; + var dontRender = _dontRender ?? false; + var macroSource = _macroSource ?? string.Empty; + + var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); + + var macro = new Macro(shortStringHelper, id, key, useInEditor, cacheDuration, alias, name, cacheByPage, cacheByMember, dontRender, macroSource); + + foreach (var property in _propertyBuilders.Select(x => x.Build())) + { + macro.Properties.Add(property); + } + + Reset(); + return macro; + } + + protected override void Reset() + { + _id = null; + _key = null; + _alias = null; + _name = null; + _useInEditor = null; + _cacheDuration = null; + _cacheByPage = null; + _cacheByMember = null; + _dontRender = null; + _macroSource = null; + _propertyBuilders = new List(); + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + string IWithAliasBuilder.Alias + { + get => _alias; + set => _alias = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs b/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs new file mode 100644 index 0000000000..3c98fef93a --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs @@ -0,0 +1,88 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Tests.Common.Builders.Extensions; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class MacroPropertyBuilder + : ChildBuilderBase, + IWithIdBuilder, + IWithKeyBuilder, + IWithAliasBuilder, + IWithNameBuilder, + IWithSortOrderBuilder + { + private int? _id; + private Guid? _key; + private string _alias; + private string _name; + private int? _sortOrder; + private string _editorAlias; + + public MacroPropertyBuilder(MacroBuilder parentBuilder) : base(parentBuilder) + { + } + + public MacroPropertyBuilder WithEditorAlias(string editorAlias) + { + _editorAlias = editorAlias; + return this; + } + + public override IMacroProperty Build() + { + var id = _id ?? 1; + var name = _name ?? Guid.NewGuid().ToString(); + var alias = _alias ?? name.ToCamelCase(); + var key = _key ?? Guid.NewGuid(); + var sortOrder = _sortOrder ?? 0; + var editorAlias = _editorAlias ?? string.Empty; + + var macroProperty = new MacroProperty(6, Guid.NewGuid(), alias, name, sortOrder, editorAlias); + + Reset(); + return macroProperty; + } + + protected override void Reset() + { + _id = null; + _key = null; + _alias = null; + _name = null; + _sortOrder = null; + _editorAlias = null; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + string IWithAliasBuilder.Alias + { + get => _alias; + set => _alias = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + + int? IWithSortOrderBuilder.SortOrder + { + get => _sortOrder; + set => _sortOrder = value; + } + } +} diff --git a/src/Umbraco.Tests/Models/MacroTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs similarity index 65% rename from src/Umbraco.Tests/Models/MacroTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs index 9ebe57a847..468b77c202 100644 --- a/src/Umbraco.Tests/Models/MacroTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs @@ -3,18 +3,36 @@ using System.Linq; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Common.Builders.Extensions; -namespace Umbraco.Tests.Models +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models { [TestFixture] public class MacroTests { + private readonly MacroBuilder _builder = new MacroBuilder(); + [Test] public void Can_Deep_Clone() { - var macro = new Macro(TestHelper.ShortStringHelper, 1, Guid.NewGuid(), true, 3, "test", "Test", false, true, true, "~/script.cshtml"); - macro.Properties.Add(new MacroProperty(6, Guid.NewGuid(), "rewq", "REWQ", 1, "asdfasdf")); + var macro = _builder + .WithId(1) + .WithUseInEditor(true) + .WithCacheDuration(3) + .WithAlias("test") + .WithName("Test") + .WithSource("~/script.cshtml") + .WithCacheByMember(true) + .WithDontRender(true) + .AddProperty() + .WithId(6) + .WithAlias("rewq") + .WithName("REWQ") + .WithSortOrder(1) + .WithEditorAlias("asdfasdf") + .Done() + .Build(); var clone = (Macro)macro.DeepClone(); @@ -24,7 +42,7 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.Properties.Count, macro.Properties.Count); - for (int i = 0; i < clone.Properties.Count; i++) + for (var i = 0; i < clone.Properties.Count; i++) { Assert.AreEqual(clone.Properties[i], macro.Properties[i]); Assert.AreNotSame(clone.Properties[i], macro.Properties[i]); @@ -37,9 +55,7 @@ namespace Umbraco.Tests.Models //This double verifies by reflection var allProps = clone.GetType().GetProperties(); foreach (var propertyInfo in allProps) - { Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(macro, null)); - } //need to ensure the event handlers are wired @@ -51,8 +67,6 @@ namespace Umbraco.Tests.Models Assert.AreEqual(1, clone.AddedProperties.Count()); clone.Properties.Remove("rewq"); Assert.AreEqual(1, clone.RemovedProperties.Count()); - } - } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MacroBuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MacroBuilderTests.cs new file mode 100644 index 0000000000..bf5d899d6a --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/MacroBuilderTests.cs @@ -0,0 +1,73 @@ +using System; +using NUnit.Framework; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Common.Builders.Extensions; + +namespace Umbraco.Tests.UnitTests.Umbraco.Tests.Common.Builders +{ + [TestFixture] + public class MacroBuilderTests + { + [Test] + public void Is_Built_Correctly() + { + // Arrange + const int id = 1; + var key = Guid.NewGuid(); + const bool useInEditor = true; + const int cacheDuration = 3; + const string alias = "test"; + const string name = "Test"; + const string source = "~/script.cshtml"; + const bool cacheByPage = false; + const bool cacheByMember = true; + const bool dontRender = true; + const int propertyId = 6; + const string propertyAlias = "rewq"; + const string propertyName = "REWQ"; + const int propertySortOrder = 1; + const string propertyEditorAlias = "asdfasdf"; + + var builder = new MacroBuilder(); + + // Act + var macro = builder + .WithId(id) + .WithKey(key) + .WithUseInEditor(useInEditor) + .WithCacheDuration(cacheDuration) + .WithAlias(alias) + .WithName(name) + .WithSource(source) + .WithCacheByPage(cacheByPage) + .WithCacheByMember(cacheByMember) + .WithDontRender(dontRender) + .AddProperty() + .WithId(propertyId) + .WithAlias(propertyAlias) + .WithName(propertyName) + .WithSortOrder(propertySortOrder) + .WithEditorAlias(propertyEditorAlias) + .Done() + .Build(); + + // Assert + Assert.AreEqual(id, macro.Id); + Assert.AreEqual(key, macro.Key); + Assert.AreEqual(useInEditor, macro.UseInEditor); + Assert.AreEqual(cacheDuration, macro.CacheDuration); + Assert.AreEqual(alias, macro.Alias); + Assert.AreEqual(name, macro.Name); + Assert.AreEqual(source, macro.MacroSource); + Assert.AreEqual(cacheByPage, macro.CacheByPage); + Assert.AreEqual(cacheByMember, macro.CacheByMember); + Assert.AreEqual(dontRender, macro.DontRender); + Assert.AreEqual(1, macro.Properties.Count); + Assert.AreEqual(propertyId, macro.Properties[0].Id); + Assert.AreEqual(propertyAlias, macro.Properties[0].Alias); + Assert.AreEqual(propertyName, macro.Properties[0].Name); + Assert.AreEqual(propertySortOrder, macro.Properties[0].SortOrder); + Assert.AreEqual(propertyEditorAlias, macro.Properties[0].EditorAlias); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 9c2e598875..26c5316e07 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -288,7 +288,6 @@ - From 597489fba2b3b17c1be23fac14ba69ccced264a7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 13 Apr 2020 19:33:39 +0200 Subject: [PATCH 26/74] Added missing builder property values when building macro properties. --- src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs b/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs index 3c98fef93a..5730bb7271 100644 --- a/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs @@ -39,7 +39,7 @@ namespace Umbraco.Tests.Common.Builders var sortOrder = _sortOrder ?? 0; var editorAlias = _editorAlias ?? string.Empty; - var macroProperty = new MacroProperty(6, Guid.NewGuid(), alias, name, sortOrder, editorAlias); + var macroProperty = new MacroProperty(id, key, alias, name, sortOrder, editorAlias); Reset(); return macroProperty; From 188d868f553e7938bf5d5f691e0c877e57ec9091 Mon Sep 17 00:00:00 2001 From: Rachel Breeze Date: Sat, 11 Apr 2020 14:45:14 +0100 Subject: [PATCH 27/74] Updated tinyMCE to 4.9.9 --- src/Umbraco.Web.UI.Client/bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index b173909c63..ab438e1d8d 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -25,7 +25,7 @@ "jquery-migrate": "1.4.0", "angular-dynamic-locale": "0.1.28", "ng-file-upload": "~7.3.8", - "tinymce": "~4.9.4", + "tinymce": "~4.9.9", "codemirror": "~5.3.0", "angular-local-storage": "~0.2.3", "moment": "~2.10.3", From ad4bce3b73e6d63191223f3ff99a356724be6e57 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 14 Apr 2020 16:26:01 +0200 Subject: [PATCH 28/74] AB#5819 - Cleanup --- src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs | 5 ----- .../Web/Mvc/RenderNoContentControllerTests.cs | 6 ++++-- src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs | 3 --- src/Umbraco.Web/Mvc/RenderNoContentController.cs | 8 ++++---- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 3a3354b322..1ea08e3118 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -333,11 +333,6 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - // we should stop injecting UmbracoContext and always inject IUmbracoContextAccessor, however at the moment - // there are tons of places (controllers...) which require UmbracoContext in their ctor - so let's register - // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context - // in it's own way but we don't want that to happen, we manage its lifetime ourselves. - composition.Register(factory => factory.GetInstance().UmbracoContext); composition.RegisterUnique(); composition.Register(factory => { diff --git a/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs index e728d75dc5..d33ce3bfcc 100644 --- a/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs @@ -3,6 +3,7 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Tests.Common; using Umbraco.Web; using Umbraco.Web.Models; using Umbraco.Web.Mvc; @@ -15,11 +16,12 @@ namespace Umbraco.Tests.Web.Mvc [Test] public void Redirects_To_Root_When_Content_Published() { + var mockUmbracoContext = new Mock(); mockUmbracoContext.Setup(x => x.Content.HasContent()).Returns(true); var mockIOHelper = new Mock(); var mockGlobalSettings = new Mock(); - var controller = new RenderNoContentController(mockUmbracoContext.Object, mockIOHelper.Object, mockGlobalSettings.Object); + var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), mockIOHelper.Object, mockGlobalSettings.Object); var result = controller.Index() as RedirectResult; @@ -40,7 +42,7 @@ namespace Umbraco.Tests.Web.Mvc var mockGlobalSettings = new Mock(); mockGlobalSettings.SetupGet(x => x.UmbracoPath).Returns(UmbracoPathSetting); mockGlobalSettings.SetupGet(x => x.NoNodesViewPath).Returns(ViewPath); - var controller = new RenderNoContentController(mockUmbracoContext.Object, mockIOHelper.Object, mockGlobalSettings.Object); + var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), mockIOHelper.Object, mockGlobalSettings.Object); var result = controller.Index() as ViewResult; Assert.IsNotNull(result); diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 0261b530ba..79c7d3ec25 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -42,9 +42,6 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); composition.RegisterMultipleUnique(); - - composition.RegisterUnique(); - } } } diff --git a/src/Umbraco.Web/Mvc/RenderNoContentController.cs b/src/Umbraco.Web/Mvc/RenderNoContentController.cs index 2ffd323440..9334591fbb 100644 --- a/src/Umbraco.Web/Mvc/RenderNoContentController.cs +++ b/src/Umbraco.Web/Mvc/RenderNoContentController.cs @@ -8,20 +8,20 @@ namespace Umbraco.Web.Mvc { public class RenderNoContentController : Controller { - private readonly IUmbracoContext _umbracoContext; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IIOHelper _ioHelper; private readonly IGlobalSettings _globalSettings; - public RenderNoContentController(IUmbracoContext umbracoContext, IIOHelper ioHelper, IGlobalSettings globalSettings) + public RenderNoContentController(IUmbracoContextAccessor umbracoContextAccessor, IIOHelper ioHelper, IGlobalSettings globalSettings) { - _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); } public ActionResult Index() { - var store = _umbracoContext.Content; + var store = _umbracoContextAccessor.UmbracoContext.Content; if (store.HasContent()) { // If there is actually content, go to the root. From 3b03d812f87a332b7ea61be9c1289665fc04ff6b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 15 Apr 2020 11:55:12 +1000 Subject: [PATCH 29/74] fixes paging --- .../Persistence/Repositories/ContentRepository.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 24c9e84873..11b56c5bf0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -862,21 +862,21 @@ order by (umbracoNode.{2}), (umbracoNode.parentID), (umbracoNode.sortOrder)", XmlElement last = null; - long pageSize = 500; + const long pageSize = 500; int? itemCount = null; - long currPage = 0; + long pageIndex = 0; do { // Get the paged queries - Database.BuildPageQueries(currPage, pageSize, sql, ref args, out var sqlCount, out var sqlPage); + Database.BuildPageQueries(pageIndex * pageSize, pageSize, sql, ref args, out var sqlCount, out var sqlPage); // get the item count once if (itemCount == null) { itemCount = Database.ExecuteScalar(sqlCount, args); } - currPage++; + pageIndex++; // iterate over rows without allocating all items to memory (Query vs Fetch) foreach (var row in Database.Query(sqlPage, args)) @@ -906,7 +906,7 @@ order by (umbracoNode.{2}), (umbracoNode.parentID), (umbracoNode.sortOrder)", last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); } - } while (itemCount == pageSize); + } while ((pageIndex * pageSize) < itemCount); return xmlDoc; From 61c39eea64c9fe7a1de4a9cb38fb89834dc754df Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 15 Apr 2020 12:49:38 +1000 Subject: [PATCH 30/74] Nicer sql template constants --- src/Umbraco.Core/Constants-SqlTemplates.cs | 17 ++++++++++------- .../Implement/ContentRepositoryBase.cs | 14 +++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index 33c94f0f93..984bc495b0 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -4,13 +4,16 @@ { public static class SqlTemplates { - public const string VersionableRepositoryGetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; - public const string VersionableRepositoryGetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; - public const string VersionableRepositoryGetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; - public const string VersionableRepositoryEnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; - public const string VersionableRepositoryGetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; - public const string VersionableRepositoryGetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; - public const string VersionableRepositoryGetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; + public static class VersionableRepository + { + public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; + public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; + public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; + public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; + public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; + public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; + public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 4aa9655249..845006891d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -83,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetVersionIds, tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => tsql.Select(x => x.Id) .From() .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) @@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the version we want to delete - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetVersion, tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) ); var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); @@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetVersions, tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) ); var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); @@ -887,7 +887,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryEnsureUniqueNodeName, tsql => tsql + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); @@ -900,7 +900,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetNewChildSortOrder(int parentId, int first) { - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetSortOrder, tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From().Where(x => x.ParentId == SqlTemplate.Arg("parentId") && x.NodeObjectType == NodeObjectTypeId) ); @@ -909,7 +909,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual NodeDto GetParentNodeDto(int parentId) { - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetParentNode, tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("parentId")) ); @@ -918,7 +918,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetReservedId(Guid uniqueId) { - var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepositoryGetReservedId, tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql.Select(x => x.NodeId).From().Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation) ); var id = Database.ExecuteScalar(template.Sql(new { uniqueId = uniqueId })); From 055f18d6398761ecdb69a8faee8c91d61e58c05a Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 15 Apr 2020 09:52:07 +0200 Subject: [PATCH 31/74] Add back ctor to revert a breaking change --- src/Umbraco.Core/Models/RelationType.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 1085ecdcdd..05caf79a99 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.Serialization; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -17,8 +18,6 @@ namespace Umbraco.Core.Models private Guid? _parentObjectType; private Guid? _childObjectType; - //TODO: Should we put back the broken ctors with obsolete attributes? - public RelationType(string alias, string name) : this(name, alias, false, null, null) { @@ -33,6 +32,15 @@ namespace Umbraco.Core.Models _childObjectType = childObjectType; } + [Obsolete("This constructor is incomplete, use one of the other constructors instead")] + public RelationType(Guid childObjectType, Guid parentObjectType, string alias) + { + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); + _childObjectType = childObjectType; + _parentObjectType = parentObjectType; + _alias = alias; + Name = _alias; + } /// From aab629808d522760323d2f562760a2f20ff0dbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 15:08:10 +0200 Subject: [PATCH 32/74] slightly more shadow on content-grid items --- .../src/less/components/umb-content-grid.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less index 622dcb8b0a..47fc8a10b9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -11,7 +11,7 @@ cursor: pointer; position: relative; user-select: none; - box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16); + box-shadow: 0 1px 2px 0 rgba(0,0,0,0.16); border-radius: 3px; } From 4a071f6e059c77dc14d3f0554e19d0ee7a5516da Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Thu, 16 Apr 2020 04:51:13 +1000 Subject: [PATCH 33/74] Nucache NullReferenceException when copying (#7961) (cherry picked from commit b30db05cc7a1b69c7e9d9990ea2677f18473bae3) # Conflicts: # src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs --- .../PublishedContent/NuCacheChildrenTests.cs | 113 ++++++++++++++---- .../PublishedCache/NuCache/ContentStore.cs | 6 +- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index cc9ffdba3c..6f79b12d4c 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.PublishedContent _snapshotService?.Dispose(); } - private void Init(IEnumerable kits) + private void Init(Func> kits) { Current.Reset(); @@ -136,7 +136,7 @@ namespace Umbraco.Tests.PublishedContent _snapshotAccessor = new TestPublishedSnapshotAccessor(); // create a data source for NuCache - _source = new TestDataSource(kits); + _source = new TestDataSource(kits()); // at last, create the complete NuCache snapshot service! var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; @@ -374,7 +374,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void EmptyTest() { - Init(Enumerable.Empty()); + Init(() => Enumerable.Empty()); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -386,7 +386,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void ChildrenTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -413,7 +413,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void ParentTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -439,7 +439,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void MoveToRootTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); // get snapshot var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); @@ -481,7 +481,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void MoveFromRootTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); // get snapshot var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); @@ -523,7 +523,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void ReOrderTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); // get snapshot var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); @@ -598,7 +598,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void MoveTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); // get snapshot var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); @@ -699,11 +699,61 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, snapshot.Content.GetById(7).Parent?.Id); } + [Test] + public void Clear_Branch_Locked() + { + // This test replicates an issue we saw here https://github.com/umbraco/Umbraco-CMS/pull/7907#issuecomment-610259393 + // The data was sent to me and this replicates it's structure + + var paths = new Dictionary { { -1, "-1" } }; + + Init(() => new List + { + CreateInvariantKit(1, -1, 1, paths), // first level + CreateInvariantKit(2, 1, 1, paths), // second level + CreateInvariantKit(3, 2, 1, paths), // third level + + CreateInvariantKit(4, 3, 1, paths), // fourth level (we'll copy this one to the same level) + + CreateInvariantKit(5, 4, 1, paths), // 6th level + + CreateInvariantKit(6, 5, 2, paths), // 7th level + CreateInvariantKit(7, 5, 3, paths), + CreateInvariantKit(8, 5, 4, paths), + CreateInvariantKit(9, 5, 5, paths), + CreateInvariantKit(10, 5, 6, paths) + }); + + // get snapshot + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var snapshotService = (PublishedSnapshotService)_snapshotService; + var contentStore = snapshotService.GetContentStore(); + //This will set a flag to force creating a new Gen next time the store is locked (i.e. In Notify) + contentStore.CreateSnapshot(); + + // notify - which ensures there are 2 generations in the cache meaning each LinkedNode has a Next value. + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(4, Guid.Empty, TreeChangeTypes.RefreshBranch) + }, out _, out _); + + // refresh the branch again, this used to show the issue where a null ref exception would occur + // because in the ClearBranchLocked logic, when SetValueLocked was called within a recursive call + // to a child, we null out the .Value of the LinkedNode within the while loop because we didn't capture + // this value before recursing. + Assert.DoesNotThrow(() => + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(4, Guid.Empty, TreeChangeTypes.RefreshBranch) + }, out _, out _)); + } + [Test] public void NestedVariationChildrenTest() { - var mixedKits = GetNestedVariantKits(); - Init(mixedKits); + Init(GetNestedVariantKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -792,7 +842,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void VariantChildrenTest() { - Init(GetVariantKits()); + Init(GetVariantKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -864,7 +914,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void RemoveTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -913,7 +963,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void UpdateTest() { - Init(GetInvariantKits()); + Init(GetInvariantKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -960,13 +1010,13 @@ namespace Umbraco.Tests.PublishedContent documents = snapshot.Content.GetById(2).Children().ToArray(); AssertDocuments(documents, "N9", "N8", "N7"); - + } [Test] public void AtRootTest() { - Init(GetVariantWithDraftKits()); + Init(GetVariantWithDraftKits); var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; @@ -995,7 +1045,7 @@ namespace Umbraco.Tests.PublishedContent yield return CreateInvariantKit(2, 1, 1, paths); } - Init(GetKits()); + Init(GetKits); var snapshotService = (PublishedSnapshotService)_snapshotService; var contentStore = snapshotService.GetContentStore(); @@ -1034,7 +1084,7 @@ namespace Umbraco.Tests.PublishedContent yield return CreateInvariantKit(4, 1, 3, paths); } - Init(GetKits()); + Init(GetKits); var snapshotService = (PublishedSnapshotService)_snapshotService; var contentStore = snapshotService.GetContentStore(); @@ -1114,7 +1164,7 @@ namespace Umbraco.Tests.PublishedContent yield return CreateInvariantKit(40, 1, 3, paths); } - Init(GetKits()); + Init(GetKits); var snapshotService = (PublishedSnapshotService)_snapshotService; var contentStore = snapshotService.GetContentStore(); @@ -1133,7 +1183,7 @@ namespace Umbraco.Tests.PublishedContent _snapshotService.Notify(new[] { - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshNode) + new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshNode) }, out _, out _); Assert.AreEqual(2, contentStore.Test.LiveGen); @@ -1181,12 +1231,12 @@ namespace Umbraco.Tests.PublishedContent //children of 1 yield return CreateInvariantKit(20, 1, 1, paths); - yield return CreateInvariantKit(30, 1, 2, paths); + yield return CreateInvariantKit(30, 1, 2, paths); yield return CreateInvariantKit(40, 1, 3, paths); } //init with all published - Init(GetKits()); + Init(GetKits); var snapshotService = (PublishedSnapshotService)_snapshotService; var contentStore = snapshotService.GetContentStore(); @@ -1203,7 +1253,7 @@ namespace Umbraco.Tests.PublishedContent //Change the root publish flag var kit = rootKit.Clone(); kit.DraftData = published ? null : kit.PublishedData; - kit.PublishedData = published? kit.PublishedData : null; + kit.PublishedData = published ? kit.PublishedData : null; _source.Kits[1] = kit; _snapshotService.Notify(new[] @@ -1218,12 +1268,12 @@ namespace Umbraco.Tests.PublishedContent var (gen, contentNode) = contentStore.Test.GetValues(1)[0]; Assert.AreEqual(assertGen, gen); //even when unpublishing/re-publishing/etc... the linked list is always maintained - AssertLinkedNode(contentNode, 100, 2, 3, 20, 40); + AssertLinkedNode(contentNode, 100, 2, 3, 20, 40); } //unpublish the root ChangePublishFlagOfRoot(false, 2, TreeChangeTypes.RefreshBranch); - + //publish the root (since it's not published, it will cause a RefreshBranch) ChangePublishFlagOfRoot(true, 3, TreeChangeTypes.RefreshBranch); @@ -1256,7 +1306,7 @@ namespace Umbraco.Tests.PublishedContent yield return CreateInvariantKit(4, 1, 3, paths); } - Init(GetKits()); + Init(GetKits); var snapshotService = (PublishedSnapshotService)_snapshotService; var contentStore = snapshotService.GetContentStore(); @@ -1312,6 +1362,17 @@ namespace Umbraco.Tests.PublishedContent AssertLinkedNode(child3.contentNode, 1, 3, -1, -1, -1); } + [Test] + public void MultipleCacheIteration() + { + //see https://github.com/umbraco/Umbraco-CMS/issues/7798 + Init(GetInvariantKits); + var snapshot = this._snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + var items = snapshot.Content.GetByXPath("/root/itype"); + Assert.AreEqual(items.Count(), items.Count()); + } private void AssertLinkedNode(ContentNode node, int parent, int prevSibling, int nextSibling, int firstChild, int lastChild) { Assert.AreEqual(parent, node.ParentContentId); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index f92d8adebb..34d21497a2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -873,9 +873,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var id = content.FirstChildContentId; while (id > 0) { + // get the required link node, this ensures that both `link` and `link.Value` are not null var link = GetRequiredLinkedNode(id, "child", null); - ClearBranchLocked(link.Value); - id = link.Value.NextSiblingContentId; + var linkValue = link.Value; // capture local since clearing in recurse can clear it + ClearBranchLocked(linkValue); // recurse + id = linkValue.NextSiblingContentId; } } From 0d7b0b96746a473a9e79f3246c32e05ad0e759da Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 15 Apr 2020 21:11:14 +0200 Subject: [PATCH 34/74] Add back ctor to revert a breaking change --- src/Umbraco.Core/Models/RelationType.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 05caf79a99..7975c39a2e 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -42,6 +42,13 @@ namespace Umbraco.Core.Models Name = _alias; } + [Obsolete("This constructor is incomplete, use one of the other constructors instead")] + public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) + : this(childObjectType, parentObjectType, alias) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + Name = name; + } /// /// Gets or sets the Name of the RelationType From b72abd50a094abe5f7f7ca3fb16be3cf890c5729 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 17:47:43 +1000 Subject: [PATCH 35/74] fixes relation type ctor --- src/Umbraco.Core/Models/RelationType.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 7975c39a2e..fafe37dfa8 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -18,6 +18,7 @@ namespace Umbraco.Core.Models private Guid? _parentObjectType; private Guid? _childObjectType; + [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(string alias, string name) : this(name, alias, false, null, null) { @@ -32,21 +33,25 @@ namespace Umbraco.Core.Models _childObjectType = childObjectType; } - [Obsolete("This constructor is incomplete, use one of the other constructors instead")] + [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(Guid childObjectType, Guid parentObjectType, string alias) { - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); + if (alias == null) throw new ArgumentNullException(nameof(alias)); + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); + _childObjectType = childObjectType; _parentObjectType = parentObjectType; _alias = alias; Name = _alias; } - [Obsolete("This constructor is incomplete, use one of the other constructors instead")] + [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) : this(childObjectType, parentObjectType, alias) { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); + if (name == null) throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + Name = name; } From feeff07d5c8d2bdc2471a6f83c3ef63e49be26a0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 18:39:02 +1000 Subject: [PATCH 36/74] Cherry pick - Possible fix for issue #7870 --- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 7dc260e4c7..8a4cec0f48 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -19,7 +19,6 @@ namespace Umbraco.Core.PropertyEditors public class DataEditor : IDataEditor { private IDictionary _defaultConfiguration; - private IDataValueEditor _dataValueEditor; /// /// Initializes a new instance of the class. @@ -91,7 +90,7 @@ namespace Umbraco.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_dataValueEditor ?? (_dataValueEditor = CreateValueEditor())); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); /// /// From 53824cac25aafe6bc99d6dbc5533fca14badfa29 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 18:46:00 +1000 Subject: [PATCH 37/74] fix isBidirectional spelling --- src/Umbraco.Core/Models/RelationType.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index fafe37dfa8..62091d090d 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Models { private string _name; private string _alias; - private bool _isBidrectional; + private bool _isBidirectional; private Guid? _parentObjectType; private Guid? _childObjectType; @@ -24,11 +24,11 @@ namespace Umbraco.Core.Models { } - public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) + public RelationType(string name, string alias, bool isBidirectional, Guid? parentObjectType, Guid? childObjectType) { _name = name; _alias = alias; - _isBidrectional = isBidrectional; + _isBidirectional = isBidirectional; _parentObjectType = parentObjectType; _childObjectType = childObjectType; } @@ -81,8 +81,8 @@ namespace Umbraco.Core.Models [DataMember] public bool IsBidirectional { - get => _isBidrectional; - set => SetPropertyValueAndDetectChanges(value, ref _isBidrectional, nameof(IsBidirectional)); + get => _isBidirectional; + set => SetPropertyValueAndDetectChanges(value, ref _isBidirectional, nameof(IsBidirectional)); } /// From d9bf7dfa9689c7d3fea4645d60bb8184ea6d96d6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 18:53:45 +1000 Subject: [PATCH 38/74] changes back isBidrectional spelling in case someone is using it with a named parameter binding :/ --- src/Umbraco.Core/Models/RelationType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 62091d090d..5def14a0c5 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -24,11 +24,11 @@ namespace Umbraco.Core.Models { } - public RelationType(string name, string alias, bool isBidirectional, Guid? parentObjectType, Guid? childObjectType) + public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) { _name = name; _alias = alias; - _isBidirectional = isBidirectional; + _isBidirectional = isBidrectional; _parentObjectType = parentObjectType; _childObjectType = childObjectType; } From 068100cc8e3348857c5a2a0c4ff2b19a36cfd619 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 16 Apr 2020 11:39:30 +0200 Subject: [PATCH 39/74] Removed resetting of unit test builders. --- .../Builders/BuilderBase.cs | 2 -- .../Builders/ConfigurationEditorBuilder.cs | 6 ---- .../Builders/DataEditorBuilder.cs | 8 ----- .../Builders/DataTypeBuilder.cs | 19 ------------ .../Builders/DataValueEditorBuilder.cs | 9 ------ .../Builders/DictionaryItemBuilder.cs | 12 -------- .../Builders/DictionaryTranslationBuilder.cs | 12 -------- .../Builders/EntitySlimBuilder.cs | 7 ----- .../Builders/GenericCollectionBuilder.cs | 14 ++------- .../Builders/GenericDictionaryBuilder.cs | 18 ++---------- .../Builders/GlobalSettingsBuilder.cs | 29 +------------------ .../Builders/LanguageBuilder.cs | 14 --------- .../Builders/MacroBuilder.cs | 16 ---------- .../Builders/MacroPropertyBuilder.cs | 15 +--------- .../Builders/MemberBuilder.cs | 25 ---------------- .../Builders/MemberGroupBuilder.cs | 11 ------- .../Builders/MemberTypeBuilder.cs | 16 ---------- .../Builders/PropertyBuilder.cs | 9 ------ .../Builders/PropertyGroupBuilder.cs | 12 -------- .../Builders/PropertyTypeBuilder.cs | 21 -------------- .../Builders/RelationBuilder.cs | 12 -------- .../Builders/RelationTypeBuilder.cs | 15 ---------- .../Builders/SmtpSettingsBuilder.cs | 9 ------ .../Builders/StylesheetBuilder.cs | 7 ----- .../Builders/TemplateBuilder.cs | 16 ---------- .../Builders/UserBuilder.cs | 26 ----------------- .../Builders/UserGroupBuilder.cs | 16 +--------- .../Models/PathValidationTests.cs | 9 +++++- 28 files changed, 16 insertions(+), 369 deletions(-) diff --git a/src/Umbraco.Tests.Common/Builders/BuilderBase.cs b/src/Umbraco.Tests.Common/Builders/BuilderBase.cs index 723581986c..d8fc048d1b 100644 --- a/src/Umbraco.Tests.Common/Builders/BuilderBase.cs +++ b/src/Umbraco.Tests.Common/Builders/BuilderBase.cs @@ -3,7 +3,5 @@ namespace Umbraco.Tests.Common.Builders public abstract class BuilderBase { public abstract T Build(); - - protected abstract void Reset(); } } diff --git a/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs b/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs index 08cb9e07f5..f0761a983e 100644 --- a/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/ConfigurationEditorBuilder.cs @@ -21,16 +21,10 @@ namespace Umbraco.Tests.Common.Builders { var defaultConfiguration = _defaultConfiguration ?? new Dictionary(); - Reset(); return new ConfigurationEditor() { DefaultConfiguration = defaultConfiguration, }; } - - protected override void Reset() - { - _defaultConfiguration = null; - } } } diff --git a/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs b/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs index b1e33f53df..5e6b174f56 100644 --- a/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs @@ -37,7 +37,6 @@ namespace Umbraco.Tests.Common.Builders var explicitConfigurationEditor = _explicitConfigurationEditorBuilder.Build(); var explicitValueEditor = _explicitValueEditorBuilder.Build(); - Reset(); return new DataEditor( Mock.Of(), Mock.Of(), @@ -51,12 +50,5 @@ namespace Umbraco.Tests.Common.Builders ExplicitValueEditor = explicitValueEditor }; } - - protected override void Reset() - { - _defaultConfiguration = null; - _explicitConfigurationEditorBuilder = new ConfigurationEditorBuilder>(this); - _explicitValueEditorBuilder = new DataValueEditorBuilder>(this); - } } } diff --git a/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs index 582c7f58b5..cdf4a68d09 100644 --- a/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs @@ -66,7 +66,6 @@ namespace Umbraco.Tests.Common.Builders var databaseType = _databaseType ?? ValueStorageType.Ntext; var sortOrder = _sortOrder ?? 0; - Reset(); return new DataType(editor, parentId) { Id = id, @@ -84,24 +83,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _dataEditorBuilder = new DataEditorBuilder(this); - _id = null; - _parentId = null; - _key = null; - _createDate = null; - _updateDate = null; - _deleteDate = null; - _name = null; - _trashed = null; - _level = null; - _path = null; - _creatorId = null; - _databaseType = null; - _sortOrder = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs b/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs index 4c271d127c..ecc2649c02 100644 --- a/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DataValueEditorBuilder.cs @@ -48,7 +48,6 @@ namespace Umbraco.Tests.Common.Builders var hideLabel = _hideLabel ?? false; var valueType = _valueType ?? Guid.NewGuid().ToString(); - Reset(); return new DataValueEditor( Mock.Of(), Mock.Of(), @@ -62,13 +61,5 @@ namespace Umbraco.Tests.Common.Builders ValueType = valueType, }; } - - protected override void Reset() - { - _configuration = null; - _view = null; - _hideLabel = null; - _valueType = null; - } } } diff --git a/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs index 17ccbcc3fb..206bccba80 100644 --- a/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DictionaryItemBuilder.cs @@ -65,7 +65,6 @@ namespace Umbraco.Tests.Common.Builders var parentId = _parentId ?? null; var itemKey = _itemKey ?? Guid.NewGuid().ToString(); - Reset(); var result = new DictionaryItem(itemKey) { Translations = _translationBuilders.Select(x => x.Build()), @@ -79,17 +78,6 @@ namespace Umbraco.Tests.Common.Builders return result; } - protected override void Reset() - { - _createDate = null; - _deleteDate = null; - _id = null; - _itemKey = null; - _key = null; - _parentId = null; - _updateDate = null; - } - public DictionaryItemBuilder WithParentId(Guid parentId) { _parentId = parentId; diff --git a/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs index 6104b005cf..6742bf0d97 100644 --- a/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/DictionaryTranslationBuilder.cs @@ -52,20 +52,8 @@ namespace Umbraco.Tests.Common.Builders Id = id }; - Reset(); return result; } - - protected override void Reset() - { - _languageBuilder = new LanguageBuilder(this); - _createDate = null; - _deleteDate = null; - _id = null; - _key = null; - _updateDate = null; - _value = null; - } DateTime? IWithCreateDateBuilder.CreateDate { diff --git a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs index f4649d6b49..844c765a9d 100644 --- a/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/EntitySlimBuilder.cs @@ -16,7 +16,6 @@ namespace Umbraco.Tests.Common.Builders var id = _id ?? 1; var parentId = _parentId ?? -1; - Reset(); return new EntitySlim { Id = id, @@ -24,12 +23,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _parentId = null; - } - public EntitySlimBuilder WithNoParentId() { _parentId = 0; diff --git a/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs b/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs index 290fdfc45b..0c45f6a599 100644 --- a/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/GenericCollectionBuilder.cs @@ -6,31 +6,21 @@ namespace Umbraco.Tests.Common.Builders public class GenericCollectionBuilder : ChildBuilderBase> { - private IList _collection; + private readonly IList _collection; public GenericCollectionBuilder(TBuilder parentBuilder) : base(parentBuilder) { + _collection = new List(); } public override IEnumerable Build() { var collection = _collection?.ToList() ?? Enumerable.Empty(); - Reset(); return collection; } - protected override void Reset() - { - _collection = null; - } - public GenericCollectionBuilder WithValue(T value) { - if (_collection == null) - { - _collection = new List(); - } - _collection.Add(value); return this; } diff --git a/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs b/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs index 444bbb61d2..f4cb7c6a30 100644 --- a/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/GenericDictionaryBuilder.cs @@ -5,34 +5,22 @@ namespace Umbraco.Tests.Common.Builders public class GenericDictionaryBuilder : ChildBuilderBase> { - private IDictionary _dictionary; + private readonly IDictionary _dictionary; public GenericDictionaryBuilder(TBuilder parentBuilder) : base(parentBuilder) { + _dictionary = new Dictionary(); } public override IDictionary Build() { - var dictionary = _dictionary == null + return _dictionary == null ? new Dictionary() : new Dictionary(_dictionary); - Reset(); - return dictionary; } - protected override void Reset() - { - _dictionary = null; - } - - public GenericDictionaryBuilder WithKeyValue(TKey key, TValue value) { - if (_dictionary == null) - { - _dictionary = new Dictionary(); - } - _dictionary.Add(key, value); return this; } diff --git a/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs b/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs index 1fe1164eba..4a88d4a571 100644 --- a/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/GlobalSettingsBuilder.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Common.Builders private string _noNodesViewPath; private bool? _useHttps; private int? _versionCheckPeriod; - private SmtpSettingsBuilder> _smtpSettingsBuilder; + private readonly SmtpSettingsBuilder> _smtpSettingsBuilder; public GlobalSettingsBuilder(TParent parentBuilder) : base(parentBuilder) { @@ -191,7 +191,6 @@ namespace Umbraco.Tests.Common.Builders var mainDomLock = _mainDomLock ?? string.Empty; var noNodesViewPath = _noNodesViewPath ?? "~/config/splashes/NoNodes.cshtml"; - Reset(); return new TestGlobalSettings { ConfigurationStatus = configurationStatus, @@ -219,32 +218,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _configurationStatus = null; - _databaseFactoryServerVersion = null; - _defaultUiLanguage = null; - _disableElectionForSingleServer = null; - _hideTopLevelNodeFromPath = null; - _installEmptyDatabase = null; - _installMissingDatabase = null; - _isSmtpServerConfigured = null; - _path = null; - _registerType = null; - _reservedPaths = null; - _reservedUrls = null; - _timeOutInMinutes = null; - _umbracoCssPath = null; - _umbracoMediaPath = null; - _umbracoPath = null; - _umbracoScriptsPath = null; - _mainDomLock = null; - _noNodesViewPath = null; - _useHttps = null; - _versionCheckPeriod = null; - _smtpSettingsBuilder = new SmtpSettingsBuilder>(this); - } - private class TestGlobalSettings : IGlobalSettings { public string ReservedUrls { get; set; } diff --git a/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs index 186b961526..953a64c5ed 100644 --- a/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/LanguageBuilder.cs @@ -66,7 +66,6 @@ namespace Umbraco.Tests.Common.Builders var isDefault = _isDefault ?? false; var isMandatory = _isMandatory ?? false; - Reset(); return new Language(Mock.Of(), cultureInfo.Name) { Id = _id ?? 1, @@ -82,19 +81,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _createDate = null; - _cultureInfo = null; - _deleteDate = null; - _fallbackLanguageId = null; - _id = null; - _isDefault = null; - _isMandatory = null; - _key = null; - _updateDate = null; - } - DateTime? IWithCreateDateBuilder.CreateDate { get => _createDate; diff --git a/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs b/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs index 953f7c9a03..66e81ef8ce 100644 --- a/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MacroBuilder.cs @@ -95,25 +95,9 @@ namespace Umbraco.Tests.Common.Builders macro.Properties.Add(property); } - Reset(); return macro; } - protected override void Reset() - { - _id = null; - _key = null; - _alias = null; - _name = null; - _useInEditor = null; - _cacheDuration = null; - _cacheByPage = null; - _cacheByMember = null; - _dontRender = null; - _macroSource = null; - _propertyBuilders = new List(); - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs b/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs index 5730bb7271..ce80a056e9 100644 --- a/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MacroPropertyBuilder.cs @@ -39,22 +39,9 @@ namespace Umbraco.Tests.Common.Builders var sortOrder = _sortOrder ?? 0; var editorAlias = _editorAlias ?? string.Empty; - var macroProperty = new MacroProperty(id, key, alias, name, sortOrder, editorAlias); - - Reset(); - return macroProperty; + return new MacroProperty(id, key, alias, name, sortOrder, editorAlias); } - protected override void Reset() - { - _id = null; - _key = null; - _alias = null; - _name = null; - _sortOrder = null; - _editorAlias = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs index f32035c5c9..16ec7ed005 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberBuilder.cs @@ -161,34 +161,9 @@ namespace Umbraco.Tests.Common.Builders member.ResetDirtyProperties(false); } - Reset(); return member; } - protected override void Reset() - { - _id = null; - _key = null; - _createDate = null; - _updateDate = null; - _name = null; - _creatorId = null; - _level = null; - _path = null; - _username = null; - _rawPasswordValue = null; - _email = null; - _failedPasswordAttempts = null; - _isApproved = null; - _isLockedOut = null; - _lastLockoutDate = null; - _lastLoginDate = null; - _lastPasswordChangeDate = null; - _sortOrder = null; - _trashed = null; - _propertyIdsIncrementingFrom = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs index 127a071083..bfd7f30a14 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberGroupBuilder.cs @@ -57,20 +57,9 @@ namespace Umbraco.Tests.Common.Builders } } - Reset(); return memberGroup; } - protected override void Reset() - { - _id = null; - _key = null; - _createDate = null; - _updateDate = null; - _name = null; - _creatorId = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs index c132163c21..d5a145331e 100644 --- a/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/MemberTypeBuilder.cs @@ -134,25 +134,9 @@ namespace Umbraco.Tests.Common.Builders memberType.ResetDirtyProperties(false); - Reset(); return memberType; } - protected override void Reset() - { - _id = null; - _alias = null; - _name = null; - _parentId = null; - _sortOrder = null; - _creatorId = null; - _description = null; - _icon = null; - _thumbnail = null; - _trashed = null; - _propertyGroupBuilders.Clear(); - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs b/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs index 6396b604e9..6cb7a431f2 100644 --- a/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/PropertyBuilder.cs @@ -36,7 +36,6 @@ namespace Umbraco.Tests.Common.Builders // Needs to be within collection to support publishing. var propertyTypeCollection = new PropertyTypeCollection(true, new[] { _propertyTypeBuilder.Build() }); - Reset(); return new Property(id, propertyTypeCollection[0]) { Key = key, @@ -45,14 +44,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _key = null; - _createDate = null; - _updateDate = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs index 25ec8bf21a..5df61dd072 100644 --- a/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/PropertyGroupBuilder.cs @@ -62,7 +62,6 @@ namespace Umbraco.Tests.Common.Builders properties.Add(propertyType); } - Reset(); return new PropertyGroup(properties) { Id = id, @@ -74,17 +73,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _key = null; - _createDate = null; - _updateDate = null; - _name = null; - _sortOrder = null; - _propertyTypeBuilders.Clear(); - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs index 3c499fb855..9a16eb50e0 100644 --- a/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/PropertyTypeBuilder.cs @@ -108,7 +108,6 @@ namespace Umbraco.Tests.Common.Builders var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); - Reset(); return new PropertyType(shortStringHelper, propertyEditorAlias, valueStorageType) { Id = id, @@ -128,26 +127,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _key = null; - _propertyEditorAlias = null; - _valueStorageType = null; - _alias = null; - _name = null; - _createDate = null; - _updateDate = null; - _sortOrder = null; - _description = null; - _dataTypeId = null; - _propertyGroupId = null; - _mandatory = null; - _mandatoryMessage = null; - _validationRegExp = null; - _validationRegExpMessage = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs b/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs index bf805aba77..2cea234200 100644 --- a/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/RelationBuilder.cs @@ -58,7 +58,6 @@ namespace Umbraco.Tests.Common.Builders var relationType = _relationTypeBuilder.Build(); - Reset(); return new Relation(parentId, childId, relationType) { Comment = comment, @@ -69,17 +68,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _parentId = null; - _childId = null; - _key = null; - _createDate = null; - _updateDate = null; - _comment = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs b/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs index dd25ae864a..243f5e3c96 100644 --- a/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs @@ -64,7 +64,6 @@ namespace Umbraco.Tests.Common.Builders var updateDate = _updateDate ?? DateTime.Now; var deleteDate = _deleteDate ?? null; - Reset(); return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType) { Id = id, @@ -75,20 +74,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _alias = null; - _childObjectType = null; - _createDate = null; - _deleteDate = null; - _id = null; - _isBidirectional = null; - _key = null; - _name = null; - _parentObjectType = null; - _updateDate = null; - } - string IWithAliasBuilder.Alias { get => _alias; diff --git a/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs b/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs index 586aedec9e..344d7bcf87 100644 --- a/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs @@ -52,7 +52,6 @@ namespace Umbraco.Tests.Common.Builders var port = _port ?? 25; var pickupDirectoryLocation = _pickupDirectoryLocation ?? null; - Reset(); return new TestSmtpSettings() { From = from, @@ -62,14 +61,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _from = null; - _host = null; - _port = null; - _pickupDirectoryLocation = null; - } - private class TestSmtpSettings : ISmtpSettings { public string From { get; set; } diff --git a/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs b/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs index c3eb097e65..ed871b9c31 100644 --- a/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/StylesheetBuilder.cs @@ -25,17 +25,10 @@ namespace Umbraco.Tests.Common.Builders var path = _path ?? string.Empty; var content = _content ?? string.Empty; - Reset(); return new Stylesheet(path) { Content = content, }; } - - protected override void Reset() - { - _path = null; - _content = null; - } } } diff --git a/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs b/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs index 6a29b89e6a..3948d285d3 100644 --- a/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/TemplateBuilder.cs @@ -58,7 +58,6 @@ namespace Umbraco.Tests.Common.Builders var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); - Reset(); return new Template(shortStringHelper, name, alias) { Id = id, @@ -73,21 +72,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _key = null; - _alias = null; - _name = null; - _createDate = null; - _updateDate = null; - _path = null; - _content = null; - _isMasterTemplate = null; - _masterTemplateAlias = null; - _masterTemplateId = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs index 4d69280ec5..9dd9ff047f 100644 --- a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -134,7 +134,6 @@ namespace Umbraco.Tests.Common.Builders var startContentIds = _startContentIds ?? new int[0]; var startMediaIds = _startMediaIds ?? new int[0]; - Reset(); return new User( globalSettings, name, @@ -160,31 +159,6 @@ namespace Umbraco.Tests.Common.Builders }; } - protected override void Reset() - { - _id = null; - _key = null; - _createDate = null; - _updateDate = null; - _language = null; - _name = null; - _username = null; - _rawPasswordValue = null; - _email = null; - _failedPasswordAttempts = null; - _isApproved = null; - _isLockedOut = null; - _lastLockoutDate = null; - _lastLoginDate = null; - _lastPasswordChangeDate = null; - _suffix = string.Empty; - _defaultLang = null; - _comments = null; - _sessionTimeout = null; - _startContentIds = null; - _startMediaIds = null; - } - int? IWithIdBuilder.Id { get => _id; diff --git a/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs index 8b0568d2d4..840cf80dd5 100644 --- a/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -68,24 +68,10 @@ namespace Umbraco.Tests.Common.Builders x.Icon == _icon && x.Permissions == _permissions && x.AllowedSections == _sectionCollection); - Reset(); return userGroup; } - protected override void Reset() - { - _startContentId = null; - _startMediaId = null; - _alias = null; - _icon = null; - _name = null; - _permissions = Enumerable.Empty(); - _sectionCollection = Enumerable.Empty(); - _suffix = null; - _id = null; - } - - int? IWithIdBuilder.Id + int? IWithIdBuilder.Id { get => _id; set => _id = value; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs index 92f728d4b8..3799d04751 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs @@ -12,7 +12,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class PathValidationTests { - private readonly EntitySlimBuilder _builder = new EntitySlimBuilder(); + private EntitySlimBuilder _builder = new EntitySlimBuilder(); [Test] public void Validate_Path() @@ -93,18 +93,25 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [Test] public void Ensure_Path_Entity_Valid_Recursive_Parent() { + // Not using the shared class-level builder as we need to reset after each usage when creating multiple entities. + _builder = new EntitySlimBuilder(); var parentA = _builder .WithId(999) .Build(); + + _builder = new EntitySlimBuilder(); var parentB = _builder .WithId(888) .WithParentId(999) .Build(); + + _builder = new EntitySlimBuilder(); var parentC = _builder .WithId(777) .WithParentId(888) .Build(); + _builder = new EntitySlimBuilder(); var entity = _builder .WithId(1234) .WithParentId(777) From 89a419e4d128e95e755a5c1e050d1da66b61737b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 16 Apr 2020 11:46:02 +0200 Subject: [PATCH 40/74] Clarified comment. --- .../Umbraco.Infrastructure/Models/PathValidationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs index 3799d04751..5f3bc98fff 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs @@ -93,7 +93,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [Test] public void Ensure_Path_Entity_Valid_Recursive_Parent() { - // Not using the shared class-level builder as we need to reset after each usage when creating multiple entities. + // Re-creating the class-level builder as we need to reset before usage when creating multiple entities. _builder = new EntitySlimBuilder(); var parentA = _builder .WithId(999) From 33853eee434b60ec6b1bae9665926ee61d2cb8e7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 16 Apr 2020 12:18:06 +0200 Subject: [PATCH 41/74] Added setup method to instantiate clean builder for all model unit tests within the class. --- .../Umbraco.Infrastructure/Models/DataTypeTests.cs | 8 +++++++- .../Models/DictionaryItemTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/LanguageTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/MacroTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/MemberGroupTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/MemberTests.cs | 8 +++++++- .../Models/PathValidationTests.cs | 11 ++++++++--- .../Models/PropertyGroupTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/PropertyTests.cs | 8 +++++++- .../Models/PropertyTypeTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/RelationTests.cs | 8 +++++++- .../Models/RelationTypeTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/StylesheetTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/TemplateTests.cs | 9 +++++++-- .../Models/UserExtensionsTests.cs | 8 +++++++- .../Umbraco.Infrastructure/Models/UserTests.cs | 8 +++++++- 16 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs index ba1e500d5a..b94effb907 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs @@ -9,7 +9,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class DataTypeTests { - private readonly DataTypeBuilder _builder = new DataTypeBuilder(); + private DataTypeBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new DataTypeBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs index 28878a2463..44a62521f3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs @@ -9,7 +9,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class DictionaryItemTests { - private readonly DictionaryItemBuilder _builder = new DictionaryItemBuilder(); + private DictionaryItemBuilder _builder = new DictionaryItemBuilder(); + + [SetUp] + public void SetUp() + { + _builder = new DictionaryItemBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs index bb82e69fa6..4dcf1a7a97 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs @@ -8,7 +8,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class LanguageTests { - private readonly LanguageBuilder _builder = new LanguageBuilder(); + private LanguageBuilder _builder = new LanguageBuilder(); + + [SetUp] + public void SetUp() + { + _builder = new LanguageBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs index 468b77c202..9137c67e7e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MacroTests.cs @@ -11,7 +11,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class MacroTests { - private readonly MacroBuilder _builder = new MacroBuilder(); + private MacroBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new MacroBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberGroupTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberGroupTests.cs index 91ccd56b9e..6c539c969a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberGroupTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberGroupTests.cs @@ -11,7 +11,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class MemberGroupTests { - private readonly MemberGroupBuilder _builder = new MemberGroupBuilder(); + private MemberGroupBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new MemberGroupBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs index 85cd84526a..73cce19cab 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/MemberTests.cs @@ -13,7 +13,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class MemberTests { - private readonly MemberBuilder _builder = new MemberBuilder(); + private MemberBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new MemberBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs index 5f3bc98fff..017d2c7774 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PathValidationTests.cs @@ -12,7 +12,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class PathValidationTests { - private EntitySlimBuilder _builder = new EntitySlimBuilder(); + private EntitySlimBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new EntitySlimBuilder(); + } [Test] public void Validate_Path() @@ -93,12 +99,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [Test] public void Ensure_Path_Entity_Valid_Recursive_Parent() { - // Re-creating the class-level builder as we need to reset before usage when creating multiple entities. - _builder = new EntitySlimBuilder(); var parentA = _builder .WithId(999) .Build(); + // Re-creating the class-level builder as we need to reset before usage when creating multiple entities. _builder = new EntitySlimBuilder(); var parentB = _builder .WithId(888) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyGroupTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyGroupTests.cs index bb7c2ec6d8..a56b0685fc 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyGroupTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyGroupTests.cs @@ -11,7 +11,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class PropertyGroupTests { - private readonly PropertyGroupBuilder _builder = new PropertyGroupBuilder(); + private PropertyGroupBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new PropertyGroupBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTests.cs index 6d0e4f8d56..0668762ab8 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTests.cs @@ -10,7 +10,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class PropertyTests { - private readonly PropertyBuilder _builder = new PropertyBuilder(); + private PropertyBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new PropertyBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTypeTests.cs index ce90f1d3b7..ac1427589b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/PropertyTypeTests.cs @@ -13,7 +13,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class PropertyTypeTests { - private readonly PropertyTypeBuilder _builder = new PropertyTypeBuilder(); + private PropertyTypeBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new PropertyTypeBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTests.cs index c21fc250d9..3a6c85c721 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTests.cs @@ -11,7 +11,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class RelationTests { - private readonly RelationBuilder _builder = new RelationBuilder(); + private RelationBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new RelationBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs index 924420fd55..5f965af386 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs @@ -9,7 +9,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class RelationTypeTests { - private readonly RelationTypeBuilder _builder = new RelationTypeBuilder(); + private RelationTypeBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new RelationTypeBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/StylesheetTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/StylesheetTests.cs index b0f94294cd..a2964cd54a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/StylesheetTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/StylesheetTests.cs @@ -10,7 +10,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class StylesheetTests { - private readonly StylesheetBuilder _builder = new StylesheetBuilder(); + private StylesheetBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new StylesheetBuilder(); + } [Test] public void Can_Create_Stylesheet() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/TemplateTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/TemplateTests.cs index df9e0c9ee1..c139e37682 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/TemplateTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/TemplateTests.cs @@ -6,7 +6,6 @@ using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Tests.Common.Builders; - using Umbraco.Tests.Common.Builders.Extensions; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models @@ -14,7 +13,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class TemplateTests { - private readonly TemplateBuilder _builder = new TemplateBuilder(); + private TemplateBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new TemplateBuilder(); + } [Test] public void Can_Deep_Clone() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs index 979d18c5a1..d3d59e77ba 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserExtensionsTests.cs @@ -13,7 +13,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class UserExtensionsTests { - private readonly UserBuilder _userBuilder = new UserBuilder(); + private UserBuilder _userBuilder; + + [SetUp] + public void SetUp() + { + _userBuilder = new UserBuilder(); + } [TestCase(-1, "-1", "-1,1,2,3,4,5", true)] // below root start node [TestCase(2, "-1,1,2", "-1,1,2,3,4,5", true)] // below start node diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs index 281f8cd040..db0761b165 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/UserTests.cs @@ -12,7 +12,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class UserTests { - private readonly UserBuilder _builder = new UserBuilder(); + private UserBuilder _builder; + + [SetUp] + public void SetUp() + { + _builder = new UserBuilder(); + } [Test] public void Can_Deep_Clone() From b09d736c1bfcb0ce285a118d8860a3d69700441d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Apr 2020 13:47:23 +0200 Subject: [PATCH 42/74] Bump verison to 8.6.1 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 1bb53004a0..760c49e436 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.6.0")] -[assembly: AssemblyInformationalVersion("8.6.0")] +[assembly: AssemblyFileVersion("8.6.1")] +[assembly: AssemblyInformationalVersion("8.6.1")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index cc6200cea7..d4a82331f5 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8600 + 8610 / - http://localhost:8600 + http://localhost:8610 False False @@ -429,4 +429,4 @@ - \ No newline at end of file + From 6cd7e5d05558bb30a33434c685d43a1982a9a7a5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 22:04:27 +1000 Subject: [PATCH 43/74] removes the extra unneeded variable --- .../Repositories/Implement/MediaRepository.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index b9ef2d4086..242f21c749 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -219,7 +219,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMedia entity) { - var media = entity; entity.AddingEntity(); // ensure unique name on the same level @@ -274,15 +273,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); - media.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; // persist the media version dto var mediaVersionDto = dto.MediaVersionDto; - mediaVersionDto.Id = media.VersionId; + mediaVersionDto.Id = entity.VersionId; Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -298,10 +297,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMedia entity) { - var media = entity; - // update - media.UpdatingEntity(); + entity.UpdatingEntity(); // Check if this entity is being moved as a descendant as part of a bulk moving operations. // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. @@ -349,9 +346,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Update(mediaVersionDto); // replace the property data - var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); + var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == entity.VersionId); Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); From d2c05bf681724aef2779d2ace62ff98fd5ca865a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 22:27:05 +1000 Subject: [PATCH 44/74] Fixes issue with RelationTypeFactory swapping the parent/child --- .../Persistence/Factories/RelationTypeFactory.cs | 2 +- .../Repositories/RelationTypeRepositoryTest.cs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs index edd87fec68..177a0494a2 100644 --- a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories public static IRelationType BuildEntity(RelationTypeDto dto) { - var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ChildObjectType, dto.ParentObjectType); + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType); try { diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs index 962737e1dc..ab06434e34 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -107,13 +107,16 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var relationType = repository.Get(RelationTypeDto.NodeIdSeed); + var relationType = repository.Get(RelationTypeDto.NodeIdSeed + 2); // Assert Assert.That(relationType, Is.Not.Null); Assert.That(relationType.HasIdentity, Is.True); - Assert.That(relationType.Alias, Is.EqualTo("relateContentOnCopy")); - Assert.That(relationType.Name, Is.EqualTo("Relate Content on Copy")); + Assert.That(relationType.IsBidirectional, Is.True); + Assert.That(relationType.Alias, Is.EqualTo("relateContentToMedia")); + Assert.That(relationType.Name, Is.EqualTo("Relate Content to Media")); + Assert.That(relationType.ChildObjectType, Is.EqualTo(Constants.ObjectTypes.Media)); + Assert.That(relationType.ParentObjectType, Is.EqualTo(Constants.ObjectTypes.Document)); } } @@ -224,8 +227,9 @@ namespace Umbraco.Tests.Persistence.Repositories public void CreateTestData() { - var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972")); - var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB")); + var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document); + var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentType); + var relateContentMedia = new RelationType("Relate Content to Media", "relateContentToMedia", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media); var provider = TestObjects.GetScopeProvider(Logger); using (var scope = ScopeProvider.CreateScope()) @@ -234,6 +238,7 @@ namespace Umbraco.Tests.Persistence.Repositories repository.Save(relateContent);//Id 2 repository.Save(relateContentType);//Id 3 + repository.Save(relateContentMedia);//Id 4 scope.Complete(); } } From 6a5366469e6cd054632f104a21e0a7b5e704b172 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 22:34:15 +1000 Subject: [PATCH 45/74] fixes null checks in ctor --- src/Umbraco.Core/Models/RelationType.cs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 5def14a0c5..16e689f719 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -20,12 +20,17 @@ namespace Umbraco.Core.Models [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(string alias, string name) - : this(name, alias, false, null, null) + : this(name: name, alias: alias, false, null, null) { } public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) { + if (name == null) throw new ArgumentNullException(nameof(alias)); + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + if (alias == null) throw new ArgumentNullException(nameof(alias)); + if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); + _name = name; _alias = alias; _isBidirectional = isBidrectional; @@ -35,24 +40,14 @@ namespace Umbraco.Core.Models [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(Guid childObjectType, Guid parentObjectType, string alias) - { - if (alias == null) throw new ArgumentNullException(nameof(alias)); - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); - - _childObjectType = childObjectType; - _parentObjectType = parentObjectType; - _alias = alias; - Name = _alias; + : this(name: alias, alias: alias, false, parentObjectType: parentObjectType, childObjectType: childObjectType) + { } [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) - : this(childObjectType, parentObjectType, alias) + : this(name: name, alias: alias, false, parentObjectType: parentObjectType, childObjectType: childObjectType) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - - Name = name; } /// From 153daa9fc5f3f6d8f2bde7c5a03213ee16cefb68 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 22:35:53 +1000 Subject: [PATCH 46/74] fixes nameof --- src/Umbraco.Core/Models/RelationType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 16e689f719..8cee2dccf2 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Models public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) { - if (name == null) throw new ArgumentNullException(nameof(alias)); + if (name == null) throw new ArgumentNullException(nameof(name)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); if (alias == null) throw new ArgumentNullException(nameof(alias)); if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(alias)); From eabc9583f05b33c38d1383f654506284d61a7690 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 22:39:32 +1000 Subject: [PATCH 47/74] adds more named params --- src/Umbraco.Core/Models/RelationType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 8cee2dccf2..25156cdb8a 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -40,13 +40,13 @@ namespace Umbraco.Core.Models [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(Guid childObjectType, Guid parentObjectType, string alias) - : this(name: alias, alias: alias, false, parentObjectType: parentObjectType, childObjectType: childObjectType) + : this(name: alias, alias: alias, isBidrectional: false, parentObjectType: parentObjectType, childObjectType: childObjectType) { } [Obsolete("This constructor is no longer used and will be removed in future versions, use one of the other constructors instead")] public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) - : this(name: name, alias: alias, false, parentObjectType: parentObjectType, childObjectType: childObjectType) + : this(name: name, alias: alias, isBidrectional: false, parentObjectType: parentObjectType, childObjectType: childObjectType) { } From 41f98a41fbc928113d703d2fb09d0b4cea26695f Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 16 Apr 2020 14:58:36 +0200 Subject: [PATCH 48/74] Added fix from #7832 since the test was somehow cherry picked into 8.6.1 already --- src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 84edb9113c..24c6a7018b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -355,6 +355,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private static IEnumerable GetByXPath(XPathNodeIterator iterator) { + iterator = iterator.Clone(); while (iterator.MoveNext()) { var xnav = iterator.Current as NavigableNavigator; From 271669581a200670916c470793816222c4c121d3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 16 Apr 2020 15:21:54 +0200 Subject: [PATCH 49/74] Merge pull request #7847 from umbraco/v8/bugfix/7773-memory-leak Fixes Memory Leak in GetCacheItem cache dependency #7773 (cherry picked from commit b8abc0441783bc161151360e24949fa09590a0be) --- src/Umbraco.Core/Cache/WebCachingAppCache.cs | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Cache/WebCachingAppCache.cs b/src/Umbraco.Core/Cache/WebCachingAppCache.cs index c6e104221a..f9be8c13a8 100644 --- a/src/Umbraco.Core/Cache/WebCachingAppCache.cs +++ b/src/Umbraco.Core/Cache/WebCachingAppCache.cs @@ -37,23 +37,13 @@ namespace Umbraco.Core.Cache /// public object Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - CacheDependency dependency = null; - if (dependentFiles != null && dependentFiles.Any()) - { - dependency = new CacheDependency(dependentFiles); - } - return Get(key, factory, timeout, isSliding, priority, removedCallback, dependency); + return GetInternal(key, factory, timeout, isSliding, priority, removedCallback, dependentFiles); } /// public void Insert(string key, Func factory, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { - CacheDependency dependency = null; - if (dependentFiles != null && dependentFiles.Any()) - { - dependency = new CacheDependency(dependentFiles); - } - Insert(key, factory, timeout, isSliding, priority, removedCallback, dependency); + InsertInternal(key, factory, timeout, isSliding, priority, removedCallback, dependentFiles); } #region Dictionary @@ -103,7 +93,7 @@ namespace Umbraco.Core.Cache #endregion - private object Get(string key, Func factory, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) + private object GetInternal(string key, Func factory, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { key = GetCacheKey(key); @@ -163,6 +153,10 @@ namespace Umbraco.Core.Cache var sliding = isSliding == false ? System.Web.Caching.Cache.NoSlidingExpiration : (timeout ?? System.Web.Caching.Cache.NoSlidingExpiration); lck.UpgradeToWriteLock(); + + // create a cache dependency if one is needed. + var dependency = dependentFiles != null && dependentFiles.Length > 0 ? new CacheDependency(dependentFiles) : null; + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(key, result, dependency, absolute, sliding, priority, removedCallback); } @@ -180,7 +174,7 @@ namespace Umbraco.Core.Cache return value; } - private void Insert(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, CacheDependency dependency = null) + private void InsertInternal(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { // NOTE - here also we must insert a Lazy but we can evaluate it right now // and make sure we don't store a null value. @@ -197,6 +191,10 @@ namespace Umbraco.Core.Cache try { _locker.EnterWriteLock(); + + // create a cache dependency if one is needed. + var dependency = dependentFiles != null && dependentFiles.Length > 0 ? new CacheDependency(dependentFiles) : null; + //NOTE: 'Insert' on System.Web.Caching.Cache actually does an add or update! _cache.Insert(cacheKey, result, dependency, absolute, sliding, priority, removedCallback); } From 3310ef9d8d392be2a57f227e33c53c002df9966d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Apr 2020 23:53:52 +1000 Subject: [PATCH 50/74] fix tests --- .../Persistence/Repositories/RelationTypeRepositoryTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs index ab06434e34..edde8e8f81 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -136,7 +136,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(relationTypes, Is.Not.Null); Assert.That(relationTypes.Any(), Is.True); Assert.That(relationTypes.Any(x => x == null), Is.False); - Assert.That(relationTypes.Count(), Is.EqualTo(7)); + Assert.That(relationTypes.Count(), Is.EqualTo(8)); } } @@ -193,7 +193,7 @@ namespace Umbraco.Tests.Persistence.Repositories int count = repository.Count(query); // Assert - Assert.That(count, Is.EqualTo(5)); + Assert.That(count, Is.EqualTo(6)); } } From 52ecd59c11742cb6015773f5d68b9f0f795a4f54 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Apr 2020 00:01:26 +1000 Subject: [PATCH 51/74] fixes test --- src/Umbraco.Tests/Services/RelationServiceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index 2ec10811b7..06de405cec 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -110,8 +110,8 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Test", rt.Name); Assert.AreEqual("repeatedEventOccurence", rt.Alias); Assert.AreEqual(false, rt.IsBidirectional); - Assert.AreEqual(Constants.ObjectTypes.Document, rt.ChildObjectType.Value); - Assert.AreEqual(Constants.ObjectTypes.Media, rt.ParentObjectType.Value); + Assert.AreEqual(Constants.ObjectTypes.Document, rt.ParentObjectType.Value); + Assert.AreEqual(Constants.ObjectTypes.Media, rt.ChildObjectType.Value); } [Test] From ee6d3e0478ff958663a733be3003b0330d119fd7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Apr 2020 00:14:05 +1000 Subject: [PATCH 52/74] Removes the alpha builds of examine from nuget.config --- NuGet.Config | 1 - 1 file changed, 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index 7d786702f4..31fd84e456 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -7,6 +7,5 @@ --> - From 14072b0c21032fb9f73ac96281d3f5c23d403cc9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Apr 2020 19:54:25 +0200 Subject: [PATCH 53/74] nuget fix --- NuGet.Config | 1 - 1 file changed, 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index 64425091dc..31fd84e456 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -7,6 +7,5 @@ --> - From 295d0b73b3b351d85e6d5299c300f1c464cc6639 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Apr 2020 20:12:06 +0200 Subject: [PATCH 54/74] nuget fix --- NuGet.Config | 1 + 1 file changed, 1 insertion(+) diff --git a/NuGet.Config b/NuGet.Config index 31fd84e456..64425091dc 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -7,5 +7,6 @@ --> + From d5e2c12685e46e55b360c385b97eb424855b54ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Apr 2020 13:20:57 +1000 Subject: [PATCH 55/74] fixes merge issue --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 57a71a2cf0..1d77505fc6 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,10 +347,7 @@ True 8700 / - http://localhost:8700 - 8610 - / - http://localhost:8610 + http://localhost:8700 False False From 9335f39495f5a4a461cdd5570b074058ee07f61b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Sat, 18 Apr 2020 22:01:39 +0100 Subject: [PATCH 56/74] Update Readme.md in netcore branch to see if LGTM.com will analyze C# --- .github/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 467ca6e5e6..d4565a8cb5 100644 --- a/.github/README.md +++ b/.github/README.md @@ -37,4 +37,3 @@ Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https:// ## Contributing Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md). - From c0ec1bf6ca9c12b6417dcf28c3f929eaed0012fa Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 20 Apr 2020 06:19:59 +0200 Subject: [PATCH 57/74] Introduced GenericDictionaryRequestAppCache, to use the HttpContext.Items from .NET Core --- .../Cache/GenericDictionaryRequestAppCache.cs | 190 ++++++++++++++++++ .../Runtime/CoreRuntime.cs | 27 ++- .../Runtime/WebRuntime.cs | 90 --------- src/Umbraco.Tests.Integration/RuntimeTests.cs | 8 +- .../Testing/UmbracoIntegrationTest.cs | 2 +- .../Routing/RenderRouteHandlerTests.cs | 6 +- .../Runtimes/CoreRuntimeTests.cs | 3 +- src/Umbraco.Tests/Runtimes/StandaloneTests.cs | 10 +- .../UmbracoCoreServiceCollectionExtensions.cs | 62 ++++-- src/Umbraco.Web/UmbracoApplication.cs | 13 +- 10 files changed, 280 insertions(+), 131 deletions(-) create mode 100644 src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs delete mode 100644 src/Umbraco.Infrastructure/Runtime/WebRuntime.cs diff --git a/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs new file mode 100644 index 0000000000..193235ca7e --- /dev/null +++ b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Cache +{ + /// + /// Implements a fast on top of HttpContext.Items. + /// + /// + /// If no current HttpContext items can be found (no current HttpContext, + /// or no Items...) then this cache acts as a pass-through and does not cache + /// anything. + /// + public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache + { + /// + /// Initializes a new instance of the class with a context, for unit tests! + /// + public GenericDictionaryRequestAppCache(Func> requestItems) : base() + { + ContextItems = requestItems; + } + + private Func> ContextItems { get; } + + public bool IsAvailable => TryGetContextItems(out _); + + private bool TryGetContextItems(out IDictionary items) + { + items = ContextItems?.Invoke(); + return items != null; + } + + /// + public override object Get(string key, Func factory) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return factory(); + + key = GetCacheKey(key); + + Lazy result; + + try + { + EnterWriteLock(); + result = items[key] as Lazy; // null if key not found + + // cannot create value within the lock, so if result.IsValueCreated is false, just + // do nothing here - means that if creation throws, a race condition could cause + // more than one thread to reach the return statement below and throw - accepted. + + if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null + { + result = SafeLazy.GetSafeLazy(factory); + items[key] = result; + } + } + finally + { + ExitWriteLock(); + } + + // using GetSafeLazy and GetSafeLazyValue ensures that we don't cache + // exceptions (but try again and again) and silently eat them - however at + // some point we have to report them - so need to re-throw here + + // this does not throw anymore + //return result.Value; + + var value = result.Value; // will not throw (safe lazy) + if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once! + return value; + } + + public bool Set(string key, object value) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return false; + key = GetCacheKey(key); + try + { + + EnterWriteLock(); + items[key] = SafeLazy.GetSafeLazy(() => value); + } + finally + { + ExitWriteLock(); + } + return true; + } + + public bool Remove(string key) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return false; + key = GetCacheKey(key); + try + { + + EnterWriteLock(); + items.Remove(key); + } + finally + { + ExitWriteLock(); + } + return true; + } + + #region Entries + + protected override IEnumerable GetDictionaryEntries() + { + const string prefix = CacheItemPrefix + "-"; + + if (!TryGetContextItems(out var items)) return Enumerable.Empty(); + + return items.Cast() + .Where(x => x.Key is string s && s.StartsWith(prefix)); + } + + protected override void RemoveEntry(string key) + { + if (!TryGetContextItems(out var items)) return; + + items.Remove(key); + } + + protected override object GetEntry(string key) + { + return !TryGetContextItems(out var items) ? null : items[key]; + } + + #endregion + + #region Lock + + private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered"; + + protected override void EnterReadLock() => EnterWriteLock(); + + protected override void EnterWriteLock() + { + if (!TryGetContextItems(out var items)) return; + + // note: cannot keep 'entered' as a class variable here, + // since there is one per request - so storing it within + // ContextItems - which is locked, so this should be safe + + var entered = false; + Monitor.Enter(items, ref entered); + items[ContextItemsLockKey] = entered; + } + + protected override void ExitReadLock() => ExitWriteLock(); + + protected override void ExitWriteLock() + { + if (!TryGetContextItems(out var items)) return; + + var entered = (bool?)items[ContextItemsLockKey] ?? false; + if (entered) + Monitor.Exit(items); + items.Remove(ContextItemsLockKey); + } + + #endregion + + public IEnumerator> GetEnumerator() + { + if (!TryGetContextItems(out var items)) + { + yield break; + } + + foreach (var item in items) + { + yield return new KeyValuePair(item.Key.ToString(), item.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 92e47771a6..1aa94ff2a9 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -25,6 +25,7 @@ namespace Umbraco.Core.Runtime private IFactory _factory; private readonly RuntimeState _state; private readonly IUmbracoBootPermissionChecker _umbracoBootPermissionChecker; + private readonly IRequestCache _requestCache; private readonly IGlobalSettings _globalSettings; private readonly IConnectionStrings _connectionStrings; @@ -39,7 +40,8 @@ namespace Umbraco.Core.Runtime IBackOfficeInfo backOfficeInfo, IDbProviderFactoryCreator dbProviderFactoryCreator, IMainDom mainDom, - ITypeFinder typeFinder) + ITypeFinder typeFinder, + IRequestCache requestCache) { IOHelper = ioHelper; Configs = configs; @@ -50,6 +52,7 @@ namespace Umbraco.Core.Runtime DbProviderFactoryCreator = dbProviderFactoryCreator; _umbracoBootPermissionChecker = umbracoBootPermissionChecker; + _requestCache = requestCache; Logger = logger; MainDom = mainDom; @@ -110,6 +113,7 @@ namespace Umbraco.Core.Runtime { if (register is null) throw new ArgumentNullException(nameof(register)); + // create and register the essential services // ie the bare minimum required to boot @@ -129,12 +133,25 @@ namespace Umbraco.Core.Runtime "Booted.", "Boot failed.")) { - Logger.Info("Booting Core"); + + Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.", + HostingEnvironment?.SiteName, + HostingEnvironment?.ApplicationId, + HostingEnvironment?.ApplicationPhysicalPath, + NetworkHelper.MachineName); Logger.Debug("Runtime: {Runtime}", GetType().FullName); // application environment ConfigureUnhandledException(); - return _factory = Configure(register, timer); + _factory = Configure(register, timer); + + // now (and only now) is the time to switch over to perWebRequest scopes. + // up until that point we may not have a request, and scoped services would + // fail to resolve - but we run Initialize within a factory scope - and then, + // here, we switch the factory to bind scopes to requests + _factory.EnablePerWebRequestScope(); + + return _factory; } } @@ -151,7 +168,7 @@ namespace Umbraco.Core.Runtime try { - + // run handlers RuntimeOptions.DoRuntimeBoot(ProfilingLogger); @@ -350,7 +367,7 @@ namespace Umbraco.Core.Runtime return new AppCaches( new DeepCloneAppCache(new ObjectCacheAppCache()), - NoAppCache.Instance, + _requestCache, new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); } diff --git a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs b/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs deleted file mode 100644 index fc2a019023..0000000000 --- a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; - -namespace Umbraco.Web.Runtime -{ - /// - /// Represents the Web Umbraco runtime. - /// - /// On top of CoreRuntime, handles all of the web-related runtime aspects of Umbraco. - public class WebRuntime : CoreRuntime - { - private readonly IRequestCache _requestCache; - - /// - /// Initializes a new instance of the class. - /// - public WebRuntime( - Configs configs, - IUmbracoVersion umbracoVersion, - IIOHelper ioHelper, - ILogger logger, - IProfiler profiler, - IHostingEnvironment hostingEnvironment, - IBackOfficeInfo backOfficeInfo, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IMainDom mainDom, - ITypeFinder typeFinder, - IRequestCache requestCache, - IUmbracoBootPermissionChecker umbracoBootPermissionChecker): - base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder) - { - _requestCache = requestCache; - } - - /// - public override IFactory Configure(IRegister register) - { - - var profilingLogger = new ProfilingLogger(Logger, Profiler); - var umbracoVersion = new UmbracoVersion(); - using (var timer = profilingLogger.TraceDuration( - $"Booting Umbraco {umbracoVersion.SemanticVersion.ToSemanticString()}.", - "Booted.", - "Boot failed.")) - { - Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.", - HostingEnvironment.SiteName, - HostingEnvironment.ApplicationId, - HostingEnvironment.ApplicationPhysicalPath, - NetworkHelper.MachineName); - Logger.Debug("Runtime: {Runtime}", GetType().FullName); - - var factory = base.Configure(register); - - // now (and only now) is the time to switch over to perWebRequest scopes. - // up until that point we may not have a request, and scoped services would - // fail to resolve - but we run Initialize within a factory scope - and then, - // here, we switch the factory to bind scopes to requests - factory.EnablePerWebRequestScope(); - - return factory; - } - - - } - - #region Getters - - protected override AppCaches GetAppCaches() => new AppCaches( - // we need to have the dep clone runtime cache provider to ensure - // all entities are cached properly (cloned in and cloned out) - new DeepCloneAppCache(new ObjectCacheAppCache()), - // we need request based cache when running in web-based context - _requestCache, - new IsolatedCaches(type => - // we need to have the dep clone runtime cache provider to ensure - // all entities are cached properly (cloned in and cloned out) - new DeepCloneAppCache(new ObjectCacheAppCache()))); - - #endregion - } -} - diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 52c29d2037..aa364210d0 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -4,8 +4,10 @@ using Moq; using NUnit.Framework; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Smidge; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Runtime; @@ -57,7 +59,7 @@ namespace Umbraco.Tests.Integration var coreRuntime = new CoreRuntime(testHelper.GetConfigs(), testHelper.GetUmbracoVersion(), testHelper.IOHelper, testHelper.Logger, testHelper.Profiler, testHelper.UmbracoBootPermissionChecker, testHelper.GetHostingEnvironment(), testHelper.GetBackOfficeInfo(), testHelper.DbProviderFactoryCreator, - testHelper.MainDom, testHelper.GetTypeFinder()); + testHelper.MainDom, testHelper.GetTypeFinder(), NoAppCache.Instance); // boot it! var factory = coreRuntime.Configure(umbracoContainer); @@ -99,7 +101,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, null, out _); }); var host = await hostBuilder.StartAsync(); @@ -138,7 +140,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, null, out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 101feb79a4..41eb082dc2 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -108,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, null, out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index b900453a5e..4763d3dbc6 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -16,10 +16,10 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Core.Strings; using Umbraco.Core.Configuration; -using Umbraco.Core.Dictionary; using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Runtime; using Umbraco.Core.Services; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.Testing; @@ -47,10 +47,10 @@ namespace Umbraco.Tests.Routing HostingEnvironment); } - public class TestRuntime : WebRuntime + public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker()) + : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache()) { } diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index 488a4f6dad..0b90457b1e 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -5,6 +5,7 @@ using Examine; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -116,7 +117,7 @@ namespace Umbraco.Tests.Runtimes public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder()) + :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance) { } diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 1a4c7f2040..5100e2e21c 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -76,7 +76,7 @@ namespace Umbraco.Tests.Runtimes var runtimeState = new RuntimeState(logger, null, umbracoVersion, backOfficeInfo); var configs = TestHelper.GetConfigs(); var variationContextAccessor = TestHelper.VariationContextAccessor; - + // create the register and the composition var register = TestHelper.GetRegister(); @@ -84,7 +84,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // determine actual runtime level runtimeState.DetermineRuntimeLevel(databaseFactory, logger); @@ -278,7 +278,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // get the components // all of them? @@ -322,8 +322,8 @@ namespace Umbraco.Tests.Runtimes Assert.AreEqual(0, results.Count); } - - + + } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 843620d571..484572f010 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Reflection; @@ -71,7 +73,13 @@ namespace Umbraco.Web.Common.Extensions var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer; - services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), out factory); + + IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); + services.AddSingleton(httpContextAccessor); + + var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items); + + services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), requestCache, httpContextAccessor, out factory); return services; } @@ -83,20 +91,28 @@ namespace Umbraco.Web.Common.Extensions /// /// /// + /// /// /// - public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly, out IFactory factory) + public static IServiceCollection AddUmbracoCore( + this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + IRegister umbContainer, + Assembly entryAssembly, + IRequestCache requestCache, + IHttpContextAccessor httpContextAccessor, + out IFactory factory) { if (services is null) throw new ArgumentNullException(nameof(services)); var container = umbContainer; if (container is null) throw new ArgumentNullException(nameof(container)); if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly)); - // Special case! The generic host adds a few default services but we need to manually add this one here NOW because - // we resolve it before the host finishes configuring in the call to CreateCompositionRoot - services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + var configs = serviceProvider.GetService(); - CreateCompositionRoot(services, webHostEnvironment, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); + + CreateCompositionRoot(services, configs, httpContextAccessor, webHostEnvironment, out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); var globalSettings = configs.Global(); var umbracoVersion = new UmbracoVersion(globalSettings); @@ -109,7 +125,8 @@ namespace Umbraco.Web.Common.Extensions profiler, hostingEnvironment, backOfficeInfo, - CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly)); + CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly), + requestCache); factory = coreRuntime.Configure(container); @@ -126,9 +143,10 @@ namespace Umbraco.Web.Common.Extensions return new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly), runtimeHash); } - private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, + private static IRuntime GetCoreRuntime( + Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, - ITypeFinder typeFinder) + ITypeFinder typeFinder, IRequestCache requestCache) { var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName]; var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator( @@ -145,22 +163,27 @@ namespace Umbraco.Web.Common.Extensions var mainDom = new MainDom(logger, mainDomLock); - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(), - hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder); + var coreRuntime = new CoreRuntime( + configs, + umbracoVersion, + ioHelper, + logger, + profiler, + new AspNetCoreBootPermissionsChecker(), + hostingEnvironment, + backOfficeInfo, + dbProviderFactoryCreator, + mainDom, + typeFinder, + requestCache); return coreRuntime; } - private static IServiceCollection CreateCompositionRoot(IServiceCollection services, IWebHostEnvironment webHostEnvironment, - out ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment, + private static IServiceCollection CreateCompositionRoot(IServiceCollection services, Configs configs, IHttpContextAccessor httpContextAccessor, IWebHostEnvironment webHostEnvironment, + out ILogger logger, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment, out IBackOfficeInfo backOfficeInfo, out IProfiler profiler) { - // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured - var serviceProvider = services.BuildServiceProvider(); - - var httpContextAccessor = serviceProvider.GetRequiredService(); - - configs = serviceProvider.GetService(); if (configs == null) throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}"); @@ -216,4 +239,5 @@ namespace Umbraco.Web.Common.Extensions } + } diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index cd72b2faf9..7679da2e2e 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,4 +1,7 @@ -using System.Threading; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Cache; @@ -34,10 +37,12 @@ namespace Umbraco.Web var mainDom = new MainDom(logger, mainDomLock); - var requestCache = new HttpRequestAppCache(() => HttpContext.Current?.Items); + var requestCache = new HttpRequestAppCache(() => HttpContext.Current != null ? HttpContext.Current.Items : null); var umbracoBootPermissionChecker = new AspNetUmbracoBootPermissionChecker(); - return new WebRuntime(configs, umbracoVersion, ioHelper, logger, profiler, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, - GetTypeFinder(hostingEnvironment, logger, profiler), requestCache, umbracoBootPermissionChecker); + return new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, + GetTypeFinder(hostingEnvironment, logger, profiler), requestCache); } + + } } From 487e3c54a8d28a40c046edde2a8b9057c60528dc Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Mon, 20 Apr 2020 22:55:40 +0200 Subject: [PATCH 58/74] Reimplementing action filters to asp.net core --- .../ActionExecutedEventArgs.cs | 17 ++++++++ .../Filters/DisableBrowserCacheAttribute.cs | 33 +++++++++++++++ .../PreRenderViewActionFilterAttribute.cs | 42 +++++++++++++++++++ .../Filters/StatusCodeResultAttribute.cs | 37 ++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs create mode 100644 src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs create mode 100644 src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs create mode 100644 src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs b/src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs new file mode 100644 index 0000000000..a2ac5701be --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.BackOffice +{ + public class ActionExecutedEventArgs : EventArgs + { + public Controller Controller { get; set; } + public object Model { get; set; } + + public ActionExecutedEventArgs(Controller controller, object model) + { + Controller = controller; + Model = model; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs new file mode 100644 index 0000000000..e2dc357fa9 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Net.Http.Headers; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext context) + { + base.OnResultExecuting(context); + + if (context.HttpContext.Response.StatusCode != 200) return; + + context.HttpContext.Response.GetTypedHeaders().CacheControl = + new CacheControlHeaderValue() + { + NoCache = true, + MaxAge = TimeSpan.Zero, + MustRevalidate = true, + NoStore = true + }; + + context.HttpContext.Response.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 + context.HttpContext.Response.Headers[HeaderNames.Pragma] = "no-cache"; + context.HttpContext.Response.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs new file mode 100644 index 0000000000..8c8a9a135a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Umbraco.Web.BackOffice.Filters +{ + public class PreRenderViewActionFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext context) + { + if (!(context.Controller is Controller umbController) || !(context.Result is ViewResult result)) + { + return; + } + + var model = result.Model; + if (model == null) + { + return; + } + + var args = new ActionExecutedEventArgs(umbController, model); + OnActionExecuted(args); + + if (args.Model != model) + { + result.ViewData.Model = args.Model; + } + + base.OnActionExecuted(context); + } + + + public static event EventHandler ActionExecuted; + + private static void OnActionExecuted(ActionExecutedEventArgs e) + { + var handler = ActionExecuted; + handler?.Invoke(null, e); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs new file mode 100644 index 0000000000..870c016b38 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using System.Net; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// Forces the response to have a specific http status code + /// + public class StatusCodeResultAttribute : ActionFilterAttribute + { + private readonly HttpStatusCode _statusCode; + + public StatusCodeResultAttribute(HttpStatusCode statusCode) + { + _statusCode = statusCode; + } + + public override void OnActionExecuted(ActionExecutedContext context) + { + base.OnActionExecuted(context); + + context.HttpContext.Response.StatusCode = (int)_statusCode; + + var disableIisCustomErrors = context.HttpContext.RequestServices.GetService().TrySkipIisCustomErrors; + var statusCodePagesFeature = context.HttpContext.Features.Get(); + + if (statusCodePagesFeature != null) + { + // if IIS Custom Errors are disabled, we won't enable the Status Code Pages + statusCodePagesFeature.Enabled = !disableIisCustomErrors; + } + } + } +} From d483f2ccbd5152425fa09db2c53a549ce0dae676 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Mon, 20 Apr 2020 22:56:36 +0200 Subject: [PATCH 59/74] Marking asp.net action filters as migrated to net core --- src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs | 1 + src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs | 1 + src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs index 227c15b344..635a7314c5 100644 --- a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs @@ -12,6 +12,7 @@ namespace Umbraco.Web.Mvc /// /// Only minifies in release mode /// + /// Migrated already to .Net Core public class MinifyJavaScriptResultAttribute : ActionFilterAttribute { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs index 54e20f5d56..2e659eccf6 100644 --- a/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs @@ -3,6 +3,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { + /// Migrated already to .Net Core public class PreRenderViewActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) diff --git a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs index b1836c6ba4..727c29b93c 100644 --- a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Mvc /// /// Forces the response to have a specific http status code /// + /// Migrated already to .Net Core internal class StatusCodeResultAttribute : ActionFilterAttribute { private readonly HttpStatusCode _statusCode; From ee37d3aa7ac354fe35d7bfc085c9872498d24764 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Apr 2020 13:07:20 +1000 Subject: [PATCH 60/74] Fixes up usage of AppDomain.CurrentDomain.BaseDirectory in log viewer which cannot work in netcore since this is inconsistent with net framework, adds notes, starts configuring serilog with UseUmbraco --- .../Hosting/IHostingEnvironment.cs | 18 +++- .../Composing/HostBuilderExtensions.cs | 4 +- .../Logging/Serilog/LoggerConfigExtensions.cs | 12 +++ .../Logging/Serilog/SerilogLogger.cs | 2 + .../Logging/Viewer/ILogViewerConfig.cs | 12 +++ .../Logging/Viewer/JsonLogViewer.cs | 21 +++-- .../Logging/Viewer/LogViewerComposer.cs | 3 +- .../Logging/Viewer/LogViewerConfig.cs | 84 +++++++++++++++++++ .../Logging/Viewer/LogViewerSourceBase.cs | 80 ++---------------- src/Umbraco.Tests/Logging/LogviewerTests.cs | 8 +- .../Extensions/HostBuilderExtensions.cs | 21 +++++ .../Umbraco.Web.Common.csproj | 1 + .../Config/logviewer.searches.config.js | 42 ++++++++++ src/Umbraco.Web.UI.NetCore/Program.cs | 3 +- .../Umbraco.Web.UI.NetCore.csproj | 8 ++ 15 files changed, 229 insertions(+), 90 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs create mode 100644 src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs create mode 100644 src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs create mode 100644 src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs index 0bdfe5c425..b653f535fa 100644 --- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs +++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs @@ -6,6 +6,10 @@ namespace Umbraco.Core.Hosting { string SiteName { get; } string ApplicationId { get; } + + /// + /// Will return the physical path to the root of the application + /// string ApplicationPhysicalPath { get; } string LocalTempPath { get; } @@ -27,10 +31,22 @@ namespace Umbraco.Core.Hosting bool IsHosted { get; } Version IISVersion { get; } + + // TODO: Should we change this name to MapPathWebRoot ? and also have a new MapPathContentRoot ? + + /// + /// Maps a virtual path to a physical path to the application's web root + /// + /// + /// + /// + /// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however + /// in netcore the web root is /www therefore this will Map to a physical path within www. + /// string MapPath(string path); /// - /// Maps a virtual path to the application's web root + /// Converts a virtual path to an absolute URL path based on the application's web root /// /// The virtual path. Must start with either ~/ or / else an exception is thrown. /// diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs index 2099778185..3ccc3b8a8d 100644 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -11,10 +11,8 @@ namespace Umbraco.Core.Composing /// Assigns a custom service provider factory to use Umbraco's container /// /// + /// /// - public static IHostBuilder UseUmbraco(this IHostBuilder builder) - => builder.UseUmbraco(new UmbracoServiceProviderFactory()); - public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory) => builder.UseServiceProviderFactory(umbracoServiceProviderFactory); } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index f4e8f85281..d810a08648 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -29,6 +29,8 @@ namespace Umbraco.Core.Logging.Serilog { global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); + // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework + //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process); @@ -57,6 +59,8 @@ namespace Umbraco.Core.Logging.Serilog /// The number of days to keep log files. Default is set to null which means all logs are kept public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) { + // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework + //Main .txt logfile - in similar format to older Log4Net output //Ends with ..txt as Date is inserted before file extension substring logConfig.WriteTo.File($@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt", @@ -85,6 +89,8 @@ namespace Umbraco.Core.Logging.Serilog Encoding encoding = null ) { + // TODO: Deal with this method call since it's obsolete, we need to change this + return configuration.Async( asyncConfiguration => asyncConfiguration.Map(AppDomainId, (_,mapConfiguration) => mapConfiguration.File( @@ -113,6 +119,8 @@ namespace Umbraco.Core.Logging.Serilog /// The number of days to keep log files. Default is set to null which means all logs are kept public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) { + // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework + //.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) //Ends with ..txt as Date is inserted before file extension substring logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json", @@ -131,6 +139,8 @@ namespace Umbraco.Core.Logging.Serilog /// A Serilog LoggerConfiguration public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig) { + // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework + //Read from main serilog.config file logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config"); @@ -144,6 +154,8 @@ namespace Umbraco.Core.Logging.Serilog /// A Serilog LoggerConfiguration public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig) { + // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework + //A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above logConfig.WriteTo.Logger(cfg => cfg.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.user.config")); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index bb77869e28..497de58bdd 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -32,6 +32,8 @@ namespace Umbraco.Core.Logging.Serilog _ioHelper = ioHelper; _marchal = marchal; + // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework + Log.Logger = new LoggerConfiguration() .ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + logConfigFile) .CreateLogger(); diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs new file mode 100644 index 0000000000..14f35361e6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Logging.Viewer +{ + public interface ILogViewerConfig + { + IReadOnlyList GetSavedSearches(); + IReadOnlyList AddSavedSearch(string name, string query); + IReadOnlyList DeleteSavedSearch(string name, string query); + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs index aea1c8fae4..54dd58ec03 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs @@ -5,6 +5,7 @@ using System.Linq; using Newtonsoft.Json; using Serilog.Events; using Serilog.Formatting.Compact.Reader; +using Umbraco.Core.Hosting; using Umbraco.Core.IO; namespace Umbraco.Core.Logging.Viewer @@ -13,14 +14,19 @@ namespace Umbraco.Core.Logging.Viewer { private readonly string _logsPath; private readonly ILogger _logger; + private readonly IHostingEnvironment _hostingEnvironment; - public JsonLogViewer(ILogger logger, IIOHelper ioHelper, string logsPath = "", string searchPath = "") : base(ioHelper, searchPath) + public JsonLogViewer(ILogger logger, ILogViewerConfig logViewerConfig, IHostingEnvironment hostingEnvironment) : base(logViewerConfig) { - if (string.IsNullOrEmpty(logsPath)) - logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\"; - - _logsPath = logsPath; + _hostingEnvironment = hostingEnvironment; _logger = logger; + + // TODO: this path is hard coded but it can actually be configured, but that is done via Serilog and we don't have a different abstraction/config + // for the logging path. We could make that, but then how would we get that abstraction into the Serilog config? I'm sure there is a way but + // don't have time right now to resolve that (since this was hard coded before). We could have a single/simple ILogConfig for umbraco that purely specifies + // the logging path and then we can have a special token that we replace in the serilog config that maps to that location? then at least we could inject + // that config in places where we are hard coding this path. + _logsPath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, @"App_Data\Logs\"); } private const int FileSizeCap = 100; @@ -62,9 +68,6 @@ namespace Umbraco.Core.Logging.Viewer { var logs = new List(); - //Log Directory - var logDirectory = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\"; - var count = 0; //foreach full day in the range - see if we can find one or more filenames that end with @@ -74,7 +77,7 @@ namespace Umbraco.Core.Logging.Viewer //Filename ending to search for (As could be multiple) var filesToFind = GetSearchPattern(day); - var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); + var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); //Foreach file we find - open it foreach (var filePath in filesForCurrentDay) diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs index 79680a3d53..e4acde1265 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs @@ -9,7 +9,8 @@ namespace Umbraco.Core.Logging.Viewer { public void Compose(Composition composition) { - composition.SetLogViewer(factory => new JsonLogViewer(composition.Logger, factory.GetInstance())); + composition.RegisterUnique(); + composition.SetLogViewer(); } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs new file mode 100644 index 0000000000..5511cd87c7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core.Hosting; +using Formatting = Newtonsoft.Json.Formatting; + +namespace Umbraco.Core.Logging.Viewer +{ + public class LogViewerConfig : ILogViewerConfig + { + private readonly IHostingEnvironment _hostingEnvironment; + private const string _pathToSearches = "~/Config/logviewer.searches.config.js"; + private readonly FileInfo _searchesConfig; + + public LogViewerConfig(IHostingEnvironment hostingEnvironment) + { + _hostingEnvironment = hostingEnvironment; + var trimmedPath = _pathToSearches.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar); + var absolutePath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, trimmedPath); + _searchesConfig = new FileInfo(absolutePath); + } + + public IReadOnlyList GetSavedSearches() + { + //Our default implementation + + //If file does not exist - lets create it with an empty array + EnsureFileExists(); + + var rawJson = System.IO.File.ReadAllText(_searchesConfig.FullName); + return JsonConvert.DeserializeObject(rawJson); + } + + public IReadOnlyList AddSavedSearch(string name, string query) + { + //Get the existing items + var searches = GetSavedSearches().ToList(); + + //Add the new item to the bottom of the list + searches.Add(new SavedLogSearch { Name = name, Query = query }); + + //Serialize to JSON string + var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented); + + //If file does not exist - lets create it with an empty array + EnsureFileExists(); + + //Write it back down to file + System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson); + + //Return the updated object - so we can instantly reset the entire array from the API response + //As opposed to push a new item into the array + return searches; + } + + public IReadOnlyList DeleteSavedSearch(string name, string query) + { + //Get the existing items + var searches = GetSavedSearches().ToList(); + + //Removes the search + searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query)); + + //Serialize to JSON string + var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented); + + //Write it back down to file + System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson); + + //Return the updated object - so we can instantly reset the entire array from the API response + return searches; + } + + private void EnsureFileExists() + { + if (_searchesConfig.Exists) return; + using (var writer = _searchesConfig.CreateText()) + { + writer.Write("[]"); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs index 4cc70eaf42..aae2976044 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs @@ -1,31 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml; -using Newtonsoft.Json; using Serilog; using Serilog.Events; -using Umbraco.Core.Composing; -using Umbraco.Core.IO; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Formatting = Newtonsoft.Json.Formatting; namespace Umbraco.Core.Logging.Viewer { + public abstract class LogViewerSourceBase : ILogViewer { - private readonly string _searchesConfigPath; - private readonly IIOHelper _ioHelper; + private readonly ILogViewerConfig _logViewerConfig; - protected LogViewerSourceBase(IIOHelper ioHelper, string pathToSearches = "") - { - if (string.IsNullOrEmpty(pathToSearches)) - // ReSharper disable once StringLiteralTypo - pathToSearches = ioHelper.MapPath("~/Config/logviewer.searches.config.js"); - - _searchesConfigPath = pathToSearches; - _ioHelper = ioHelper; + protected LogViewerSourceBase(ILogViewerConfig logViewerConfig) + { + _logViewerConfig = logViewerConfig; } public abstract bool CanHandleLargeLogs { get; } @@ -38,55 +27,13 @@ namespace Umbraco.Core.Logging.Viewer public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); public virtual IReadOnlyList GetSavedSearches() - { - //Our default implementation - - //If file does not exist - lets create it with an empty array - EnsureFileExists(_searchesConfigPath, "[]", _ioHelper); - - var rawJson = System.IO.File.ReadAllText(_searchesConfigPath); - return JsonConvert.DeserializeObject(rawJson); - } + => _logViewerConfig.GetSavedSearches(); public virtual IReadOnlyList AddSavedSearch(string name, string query) - { - //Get the existing items - var searches = GetSavedSearches().ToList(); - - //Add the new item to the bottom of the list - searches.Add(new SavedLogSearch { Name = name, Query = query }); - - //Serialize to JSON string - var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented); - - //If file does not exist - lets create it with an empty array - EnsureFileExists(_searchesConfigPath, "[]", _ioHelper); - - //Write it back down to file - System.IO.File.WriteAllText(_searchesConfigPath, rawJson); - - //Return the updated object - so we can instantly reset the entire array from the API response - //As opposed to push a new item into the array - return searches; - } + => _logViewerConfig.AddSavedSearch(name, query); public virtual IReadOnlyList DeleteSavedSearch(string name, string query) - { - //Get the existing items - var searches = GetSavedSearches().ToList(); - - //Removes the search - searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query)); - - //Serialize to JSON string - var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented); - - //Write it back down to file - System.IO.File.WriteAllText(_searchesConfigPath, rawJson); - - //Return the updated object - so we can instantly reset the entire array from the API response - return searches; - } + => _logViewerConfig.DeleteSavedSearch(name, query); public int GetNumberOfErrors(LogTimePeriod logTimePeriod) { @@ -182,15 +129,6 @@ namespace Umbraco.Core.Logging.Viewer }; } - private static void EnsureFileExists(string path, string contents, IIOHelper ioHelper) - { - var absolutePath = ioHelper.MapPath(path); - if (System.IO.File.Exists(absolutePath)) return; - - using (var writer = System.IO.File.CreateText(absolutePath)) - { - writer.Write(contents); - } - } + } } diff --git a/src/Umbraco.Tests/Logging/LogviewerTests.cs b/src/Umbraco.Tests/Logging/LogviewerTests.cs index 87cc19a2c6..75ff81a6d5 100644 --- a/src/Umbraco.Tests/Logging/LogviewerTests.cs +++ b/src/Umbraco.Tests/Logging/LogviewerTests.cs @@ -33,13 +33,14 @@ namespace Umbraco.Tests.Logging //Create an example JSON log file to check results //As a one time setup for all tets in this class/fixture var ioHelper = TestHelper.IOHelper; + var hostingEnv = TestHelper.GetHostingEnvironment(); var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName); - _newLogfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"App_Data\Logs\"); + _newLogfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"App_Data\Logs\"); _newLogfilePath = Path.Combine(_newLogfileDirPath, _logfileName); var exampleSearchfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _searchfileName); - _newSearchfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Config\"); + _newSearchfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"Config\"); _newSearchfilePath = Path.Combine(_newSearchfileDirPath, _searchfileName); //Create/ensure Directory exists @@ -51,7 +52,8 @@ namespace Umbraco.Tests.Logging File.Copy(exampleSearchfilePath, _newSearchfilePath, true); var logger = Mock.Of(); - _logViewer = new JsonLogViewer(logger, ioHelper, logsPath: _newLogfileDirPath, searchPath: _newSearchfilePath); + var logViewerConfig = new LogViewerConfig(hostingEnv); + _logViewer = new JsonLogViewer(logger, logViewerConfig, hostingEnv); } [OneTimeTearDown] diff --git a/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs new file mode 100644 index 0000000000..3cb0922837 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Hosting; +using Serilog; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Extensions +{ + public static class HostBuilderExtensions + { + /// + /// Assigns a custom service provider factory to use Umbraco's container + /// + /// + /// + public static IHostBuilder UseUmbraco(this IHostBuilder builder) + { + return builder + .UseSerilog() + .UseUmbraco(new UmbracoServiceProviderFactory()); + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 7203c4ba29..711659730f 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js b/src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js new file mode 100644 index 0000000000..345fe23764 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/logviewer.searches.config.js @@ -0,0 +1,42 @@ +[ + { + "name": "Find all logs where the Level is NOT Verbose and NOT Debug", + "query": "Not(@Level='Verbose') and Not(@Level='Debug')" + }, + { + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", + "query": "Has(@Exception)" + }, + { + "name": "Find all logs that have the property 'Duration'", + "query": "Has(Duration)" + }, + { + "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + "query": "Has(Duration) and Duration > 1000" + }, + { + "name": "Find all logs that are from the namespace 'Umbraco.Core'", + "query": "StartsWith(SourceContext, 'Umbraco.Core')" + }, + { + "name": "Find all logs that use a specific log message template", + "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" + }, + { + "name": "Find logs where one of the items in the SortedComponentTypes property array is equal to", + "query": "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" + }, + { + "name": "Find logs where one of the items in the SortedComponentTypes property array contains", + "query": "Contains(SortedComponentTypes[?], 'DatabaseServer')" + }, + { + "name": "Find all logs that the message has localhost in it with SQL like", + "query": "@Message like '%localhost%'" + }, + { + "name": "Find all logs that the message that starts with 'end' in it with SQL like", + "query": "@Message like 'end%'" + } +] diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index f0504d77e3..1fe66f6664 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -1,8 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Serilog; using Umbraco.Core.Composing; +using Umbraco.Web.Common.Extensions; namespace Umbraco.Web.UI.BackOffice { diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 72f29f3c4b..2a3b90d5f1 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -35,4 +35,12 @@ + + + + + + + + From d0f1ea0e595f31c97536b400a5a0bfeacc3899de Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Apr 2020 13:31:55 +1000 Subject: [PATCH 61/74] Gets logging working in netcore, removes remaining usages of AppDomain.CurrentDomain.BaseDirectory and reduces more IOHelper usages --- .gitignore | 1 + .../Diagnostics/MiniDump.cs | 9 ++-- .../Logging/Serilog/LoggerConfigExtensions.cs | 29 +++++-------- .../Logging/Serilog/SerilogLogger.cs | 24 +++++------ src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 +- .../UmbracoExamine/ExamineBaseTest.cs | 2 +- .../Config/serilog.Release.config | 41 +++++++++++++++++++ .../Config/serilog.config | 41 +++++++++++++++++++ .../Config/serilog.user.Release.config | 39 ++++++++++++++++++ .../Config/serilog.user.config | 39 ++++++++++++++++++ .../Umbraco.Web.UI.NetCore.csproj | 25 +++++++++++ 11 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config create mode 100644 src/Umbraco.Web.UI.NetCore/Config/serilog.config create mode 100644 src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config create mode 100644 src/Umbraco.Web.UI.NetCore/Config/serilog.user.config diff --git a/.gitignore b/.gitignore index 5f2432313f..d8c3f27d5a 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ build/temp/ /src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/lib/* /src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/views/* /src/Umbraco.Web.UI.NetCore/wwwroot/App_Data/TEMP/* +/src/Umbraco.Web.UI.NetCore/App_Data/Logs/* diff --git a/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs b/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs index 9bc0b1c3fb..57e9b5204b 100644 --- a/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs +++ b/src/Umbraco.Infrastructure/Diagnostics/MiniDump.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Umbraco.Core.Composing; +using Umbraco.Core.Hosting; using Umbraco.Core.IO; namespace Umbraco.Core.Diagnostics @@ -100,7 +101,7 @@ namespace Umbraco.Core.Diagnostics return bRet; } - public static bool Dump(IMarchal marchal, IIOHelper ioHelper, Option options = Option.WithFullMemory, bool withException = false) + public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false) { lock (LockO) { @@ -110,7 +111,7 @@ namespace Umbraco.Core.Diagnostics // filter everywhere in our code = not! var stacktrace = withException ? Environment.StackTrace : string.Empty; - var filepath = ioHelper.MapPath("~/App_Data/MiniDump"); + var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump"); if (Directory.Exists(filepath) == false) Directory.CreateDirectory(filepath); @@ -122,11 +123,11 @@ namespace Umbraco.Core.Diagnostics } } - public static bool OkToDump(IIOHelper ioHelper) + public static bool OkToDump(IHostingEnvironment hostingEnvironment) { lock (LockO) { - var filepath = ioHelper.MapPath("~/App_Data/MiniDump"); + var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump"); if (Directory.Exists(filepath) == false) return true; var count = Directory.GetFiles(filepath, "*.dmp").Length; return count < 8; diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index d810a08648..b0521c6da8 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Text; using Serilog; using Serilog.Configuration; @@ -29,11 +30,9 @@ namespace Umbraco.Core.Logging.Serilog { global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); - // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework - //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> - Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.ApplicationPhysicalPath, EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) @@ -57,13 +56,11 @@ namespace Umbraco.Core.Logging.Serilog /// A Serilog LoggerConfiguration /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept - public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) + public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) { - // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework - //Main .txt logfile - in similar format to older Log4Net output //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File($@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt", + logConfig.WriteTo.File(Path.Combine(hostingEnvironment.ApplicationPhysicalPath, $@"App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt"), shared: true, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: minimumLevel, @@ -117,13 +114,11 @@ namespace Umbraco.Core.Logging.Serilog /// A Serilog LoggerConfiguration /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept - public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) + public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) { - // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework - //.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json", + logConfig.WriteTo.File(new CompactJsonFormatter(), Path.Combine(hostingEnvironment.ApplicationPhysicalPath, $@"App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json"), shared: true, rollingInterval: RollingInterval.Day, //Create a new JSON file every day retainedFileCountLimit: retainedFileCount, //Setting to null means we keep all files - default is 31 days @@ -137,12 +132,10 @@ namespace Umbraco.Core.Logging.Serilog /// That allows the main logging pipeline to be configured /// /// A Serilog LoggerConfiguration - public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig) + public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment) { - // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework - //Read from main serilog.config file - logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config"); + logConfig.ReadFrom.AppSettings(filePath: Path.Combine(hostingEnvironment.ApplicationPhysicalPath, @"config\serilog.config")); return logConfig; } @@ -152,13 +145,11 @@ namespace Umbraco.Core.Logging.Serilog /// That allows a separate logging pipeline to be configured that will not affect the main Umbraco log /// /// A Serilog LoggerConfiguration - public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig) + public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment) { - // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework - //A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above logConfig.WriteTo.Logger(cfg => - cfg.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.user.config")); + cfg.ReadFrom.AppSettings(filePath: Path.Combine(hostingEnvironment.ApplicationPhysicalPath, @"config\serilog.user.config"))); return logConfig; } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 497de58bdd..e4695dedd1 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -19,30 +19,28 @@ namespace Umbraco.Core.Logging.Serilog public class SerilogLogger : ILogger, IDisposable { private readonly ICoreDebugSettings _coreDebugSettings; - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IMarchal _marchal; /// /// Initialize a new instance of the class with a configuration file. /// /// - public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, FileInfo logConfigFile) + public SerilogLogger(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal, FileInfo logConfigFile) { _coreDebugSettings = coreDebugSettings; - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; _marchal = marchal; - // TODO: we can't use AppDomain.CurrentDomain.BaseDirectory, need a consistent approach between netcore/netframework - Log.Logger = new LoggerConfiguration() - .ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + logConfigFile) + .ReadFrom.AppSettings(filePath: logConfigFile.FullName) .CreateLogger(); } - public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, LoggerConfiguration logConfig) + public SerilogLogger(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal, LoggerConfiguration logConfig) { _coreDebugSettings = coreDebugSettings; - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; _marchal = marchal; //Configure Serilog static global logger with config passed in @@ -58,10 +56,10 @@ namespace Umbraco.Core.Logging.Serilog var loggerConfig = new LoggerConfiguration(); loggerConfig .MinimalConfiguration(hostingEnvironment, sessionIdResolver, requestCacheGetter) - .ReadFromConfigFile() - .ReadFromUserConfigFile(); + .ReadFromConfigFile(hostingEnvironment) + .ReadFromUserConfigFile(hostingEnvironment); - return new SerilogLogger(coreDebugSettings, ioHelper, marchal, loggerConfig); + return new SerilogLogger(coreDebugSettings, hostingEnvironment, marchal, loggerConfig); } /// @@ -184,14 +182,14 @@ namespace Umbraco.Core.Logging.Serilog dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception); // dump if it is ok to dump (might have a cap on number of dump...) - dump &= MiniDump.OkToDump(_ioHelper); + dump &= MiniDump.OkToDump(_hostingEnvironment); } if (dump) { try { - var dumped = MiniDump.Dump(_marchal, _ioHelper, withException: true); + var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); messageTemplate += dumped ? "\r\nA minidump was created in App_Data/MiniDump" : "\r\nFailed to create a minidump"; diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 901192c609..f62a69177a 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -264,7 +264,7 @@ namespace Umbraco.Tests.Testing profiler = Mock.Of(); break; case UmbracoTestOptions.Logger.Serilog: - logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); + logger = new SerilogLogger(TestHelper.CoreDebugSettings, HostingEnvironment, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); profiler = new LogProfiler(logger); break; case UmbracoTestOptions.Logger.Console: diff --git a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs index a6c544062e..76b989a7a3 100644 --- a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.UmbracoExamine public void InitializeFixture() { - var logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); + var logger = new SerilogLogger(TestHelper.CoreDebugSettings, HostingEnvironment, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); _profilingLogger = new ProfilingLogger(logger, new LogProfiler(logger)); } diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config new file mode 100644 index 0000000000..e3cf52b3c5 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.config new file mode 100644 index 0000000000..e3cf52b3c5 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config new file mode 100644 index 0000000000..8f207406e3 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.Release.config @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.user.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.config new file mode 100644 index 0000000000..8f207406e3 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.user.config @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 2a3b90d5f1..59bb76bb7a 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -35,6 +35,11 @@ + + + + + @@ -43,4 +48,24 @@ + + + Designer + serilog.config + + + Designer + serilog.user.config + + + + + + Designer + + + Designer + + + From 77a06efa911c4bbe2174aeba639946d8d850c9b6 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 09:19:18 +0200 Subject: [PATCH 62/74] Moved filters to Umbraco.Web.Common project --- .../Events}/ActionExecutedEventArgs.cs | 2 +- .../Filters/DisableBrowserCacheAttribute.cs | 14 ++++++++------ .../Filters/PreRenderViewActionFilterAttribute.cs | 3 ++- .../Filters/StatusCodeResultAttribute.cs | 14 ++++++++------ 4 files changed, 19 insertions(+), 14 deletions(-) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common/Events}/ActionExecutedEventArgs.cs (91%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Filters/DisableBrowserCacheAttribute.cs (57%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Filters/PreRenderViewActionFilterAttribute.cs (93%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Filters/StatusCodeResultAttribute.cs (67%) diff --git a/src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs similarity index 91% rename from src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs rename to src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs index a2ac5701be..b33cbc7d8a 100644 --- a/src/Umbraco.Web.BackOffice/ActionExecutedEventArgs.cs +++ b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs @@ -1,7 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc; -namespace Umbraco.Web.BackOffice +namespace Umbraco.Web.Common.Events { public class ActionExecutedEventArgs : EventArgs { diff --git a/src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs similarity index 57% rename from src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs rename to src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs index e2dc357fa9..0fe251bac4 100644 --- a/src/Umbraco.Web.BackOffice/Filters/DisableBrowserCacheAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Net.Http.Headers; -namespace Umbraco.Web.BackOffice.Filters +namespace Umbraco.Web.Common.Filters { /// /// Ensures that the request is not cached by the browser @@ -14,9 +14,11 @@ namespace Umbraco.Web.BackOffice.Filters { base.OnResultExecuting(context); - if (context.HttpContext.Response.StatusCode != 200) return; + var httpResponse = context.HttpContext.Response; - context.HttpContext.Response.GetTypedHeaders().CacheControl = + if (httpResponse.StatusCode != 200) return; + + httpResponse.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() { NoCache = true, @@ -25,9 +27,9 @@ namespace Umbraco.Web.BackOffice.Filters NoStore = true }; - context.HttpContext.Response.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 - context.HttpContext.Response.Headers[HeaderNames.Pragma] = "no-cache"; - context.HttpContext.Response.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); + httpResponse.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 + httpResponse.Headers[HeaderNames.Pragma] = "no-cache"; + httpResponse.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs similarity index 93% rename from src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs rename to src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs index 8c8a9a135a..2ba58a8fd7 100644 --- a/src/Umbraco.Web.BackOffice/Filters/PreRenderViewActionFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs @@ -1,8 +1,9 @@ using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Web.Common.Events; -namespace Umbraco.Web.BackOffice.Filters +namespace Umbraco.Web.Common.Filters { public class PreRenderViewActionFilterAttribute : ActionFilterAttribute { diff --git a/src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs similarity index 67% rename from src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs rename to src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs index 870c016b38..8f3fcf3a95 100644 --- a/src/Umbraco.Web.BackOffice/Filters/StatusCodeResultAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs @@ -1,10 +1,10 @@ -using Microsoft.AspNetCore.Mvc.Filters; -using System.Net; +using System.Net; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.Configuration.UmbracoSettings; -namespace Umbraco.Web.BackOffice.Filters +namespace Umbraco.Web.Common.Filters { /// /// Forces the response to have a specific http status code @@ -22,10 +22,12 @@ namespace Umbraco.Web.BackOffice.Filters { base.OnActionExecuted(context); - context.HttpContext.Response.StatusCode = (int)_statusCode; + var httpContext = context.HttpContext; - var disableIisCustomErrors = context.HttpContext.RequestServices.GetService().TrySkipIisCustomErrors; - var statusCodePagesFeature = context.HttpContext.Features.Get(); + httpContext.Response.StatusCode = (int)_statusCode; + + var disableIisCustomErrors = httpContext.RequestServices.GetService().TrySkipIisCustomErrors; + var statusCodePagesFeature = httpContext.Features.Get(); if (statusCodePagesFeature != null) { From 65b45eb48d41430a437444007ffe8a43eef63722 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 09:25:15 +0200 Subject: [PATCH 63/74] Marking classes as moved --- src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs | 1 + src/Umbraco.Web/Mvc/Constants.cs | 1 + .../Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs | 1 + src/Umbraco.Web/Mvc/IRenderController.cs | 1 + 4 files changed, 4 insertions(+) diff --git a/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs b/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs index 1c596ff80c..6904aa103a 100644 --- a/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs +++ b/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs @@ -3,6 +3,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { + /// Migrated already to .Net Core public class ActionExecutedEventArgs : EventArgs { public Controller Controller { get; set; } diff --git a/src/Umbraco.Web/Mvc/Constants.cs b/src/Umbraco.Web/Mvc/Constants.cs index 1794345746..c71ed6b104 100644 --- a/src/Umbraco.Web/Mvc/Constants.cs +++ b/src/Umbraco.Web/Mvc/Constants.cs @@ -3,6 +3,7 @@ /// /// constants /// + /// Migrated already to .Net Core internal static class Constants { internal const string ViewLocation = "~/Views"; diff --git a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs index 34b857dfb4..f443abbb70 100644 --- a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.Mvc /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains /// a partial view macro form. /// + /// Migrated already to .Net Core internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute { /// diff --git a/src/Umbraco.Web/Mvc/IRenderController.cs b/src/Umbraco.Web/Mvc/IRenderController.cs index 103a484869..0de585959c 100644 --- a/src/Umbraco.Web/Mvc/IRenderController.cs +++ b/src/Umbraco.Web/Mvc/IRenderController.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.Mvc /// /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking /// + /// Migrated already to .Net Core public interface IRenderController : IController { From 138b8099fc119408241b5d1ba5f6470affae2981 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 09:28:22 +0200 Subject: [PATCH 64/74] Reimplementing EnsurePartialViewMacroViewContextFilterAttribute and other required classes --- src/Umbraco.Web.Common/Constants/Constants.cs | 12 +++ .../Controllers/RenderController.cs | 9 ++ ...tialViewMacroViewContextFilterAttribute.cs | 87 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/Umbraco.Web.Common/Constants/Constants.cs create mode 100644 src/Umbraco.Web.Common/Controllers/RenderController.cs create mode 100644 src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs diff --git a/src/Umbraco.Web.Common/Constants/Constants.cs b/src/Umbraco.Web.Common/Constants/Constants.cs new file mode 100644 index 0000000000..1acbe761c1 --- /dev/null +++ b/src/Umbraco.Web.Common/Constants/Constants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.Common.Constants +{ + /// + /// constants + /// + internal static class Constants + { + internal const string ViewLocation = "~/Views"; + + internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; + } +} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs new file mode 100644 index 0000000000..43058616de --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Controllers +{ + public abstract class RenderController : Controller + { + + } +} diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs new file mode 100644 index 0000000000..bddb8b3653 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -0,0 +1,87 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Web.Common.Controllers; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// This is a special filter which is required for the RTE to be able to render Partial View Macros that + /// contain forms when the RTE value is resolved outside of an MVC view being rendered + /// + /// + /// The entire way that we support partial view macros that contain forms isn't really great, these forms + /// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action + /// so the form can show errors, viewdata, etc... + /// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases + /// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly + /// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is + /// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. + /// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that + /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains + /// a partial view macro form. + /// + internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + { + + /// + /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, + /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still + /// render any PartialViewMacro with a form and maintain ModelState + /// + /// + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + /// + /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, + /// this ensures that any custom ModelState that may have been added in the RenderController itself is + /// passed onwards in case it is required when rendering a PartialViewMacro with a form + /// + /// The filter context. + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + private void SetViewContext(ActionContext context, Controller controller) + { + var viewCtx = new ViewContext( + context, + new DummyView(), + controller.ViewData, + controller.TempData, + new StringWriter(), + new HtmlHelperOptions()); + + //set the special data token + context.RouteData.DataTokens[Constants.Constants.DataTokenCurrentViewContext] = viewCtx; + } + + private class DummyView : IView + { + public Task RenderAsync(ViewContext context) + { + return Task.CompletedTask; + } + + public string Path { get; } + } + } +} From 16aab7a3cbd4a2d6d39cac14ea5f3f5b94c4480e Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 09:33:39 +0200 Subject: [PATCH 65/74] Refactoring --- .../Filters/MinifyJavaScriptResultAttribute.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs index 0ed0fd658a..65c2be051f 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs @@ -2,9 +2,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core.Hosting; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Runtime; using Umbraco.Core.WebAssets; -using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.ActionResults; namespace Umbraco.Web.BackOffice.Filters @@ -14,10 +12,11 @@ namespace Umbraco.Web.BackOffice.Filters public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // logic before action goes here - var hostingEnvironment = context.HttpContext.RequestServices.GetService(); + var serviceProvider = context.HttpContext.RequestServices; + var hostingEnvironment = serviceProvider.GetService(); if (!hostingEnvironment.IsDebugMode) { - var runtimeMinifier = context.HttpContext.RequestServices.GetService(); + var runtimeMinifier = serviceProvider.GetService(); if (context.Result is JavaScriptResult jsResult) { From 28f5033cb727244747077cd13dd11bc9a5e7780c Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 10:14:03 +0200 Subject: [PATCH 66/74] Renaming a file for constants. Clarifying the proj for other Constants file --- .../Constants/{Constants.cs => ViewConstants.cs} | 2 +- .../Extensions/UmbracoCoreServiceCollectionExtensions.cs | 4 ++-- .../EnsurePartialViewMacroViewContextFilterAttribute.cs | 3 ++- .../RuntimeMinification/SmidgeRuntimeMinifier.cs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) rename src/Umbraco.Web.Common/Constants/{Constants.cs => ViewConstants.cs} (86%) diff --git a/src/Umbraco.Web.Common/Constants/Constants.cs b/src/Umbraco.Web.Common/Constants/ViewConstants.cs similarity index 86% rename from src/Umbraco.Web.Common/Constants/Constants.cs rename to src/Umbraco.Web.Common/Constants/ViewConstants.cs index 1acbe761c1..5da1a74f55 100644 --- a/src/Umbraco.Web.Common/Constants/Constants.cs +++ b/src/Umbraco.Web.Common/Constants/ViewConstants.cs @@ -3,7 +3,7 @@ /// /// constants /// - internal static class Constants + internal static class ViewConstants { internal const string ViewLocation = "~/Views"; diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 843620d571..74477bbe6c 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -130,7 +130,7 @@ namespace Umbraco.Web.Common.Extensions IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, ITypeFinder typeFinder) { - var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName]; + var connectionStringConfig = configs.ConnectionStrings()[Core.Constants.System.UmbracoConnectionName]; var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator( connectionStringConfig?.ProviderName, DbProviderFactories.GetFactory); @@ -185,7 +185,7 @@ namespace Umbraco.Web.Common.Extensions public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services, IConfiguration configuration) { - services.AddSmidge(configuration.GetSection(Constants.Configuration.ConfigRuntimeMinification)); + services.AddSmidge(configuration.GetSection(Core.Constants.Configuration.ConfigRuntimeMinification)); services.AddSmidgeNuglify(); return services; diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs index bddb8b3653..e2046209d5 100644 --- a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Web.Common.Constants; using Umbraco.Web.Common.Controllers; namespace Umbraco.Web.Common.Filters @@ -71,7 +72,7 @@ namespace Umbraco.Web.Common.Filters new HtmlHelperOptions()); //set the special data token - context.RouteData.DataTokens[Constants.Constants.DataTokenCurrentViewContext] = viewCtx; + context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; } private class DummyView : IView diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs index ba3c6c289f..c4b9522099 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Common.RuntimeMinification public void Reset() { var version = DateTime.UtcNow.Ticks.ToString(); - _configManipulator.SaveConfigValue(Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); + _configManipulator.SaveConfigValue(Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); } From c185b67ed0551d61f04866ec4a927d1f5a957d15 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 10:55:01 +0200 Subject: [PATCH 67/74] Adding UseStatusCodePages for disabling/enabling status code pages (ex: from IIS) --- src/Umbraco.Web.UI.NetCore/Startup.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 75b2d6f48e..b986ac2c17 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -75,6 +75,7 @@ namespace Umbraco.Web.UI.BackOffice { app.UseDeveloperExceptionPage(); } + app.UseStatusCodePages(); app.UseUmbracoCore(); app.UseUmbracoWebsite(); app.UseUmbracoBackOffice(); From fbe6f6cdd2836f6fe82385c60adfcda47a05f700 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 21 Apr 2020 11:00:30 +0200 Subject: [PATCH 68/74] AB#6232 - Moved EnablePerWebRequestScope from configure to start --- src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 1aa94ff2a9..ebd91f52a2 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -145,12 +145,6 @@ namespace Umbraco.Core.Runtime ConfigureUnhandledException(); _factory = Configure(register, timer); - // now (and only now) is the time to switch over to perWebRequest scopes. - // up until that point we may not have a request, and scoped services would - // fail to resolve - but we run Initialize within a factory scope - and then, - // here, we switch the factory to bind scopes to requests - _factory.EnablePerWebRequestScope(); - return _factory; } } @@ -261,6 +255,13 @@ namespace Umbraco.Core.Runtime // create & initialize the components _components = _factory.GetInstance(); _components.Initialize(); + + + // now (and only now) is the time to switch over to perWebRequest scopes. + // up until that point we may not have a request, and scoped services would + // fail to resolve - but we run Initialize within a factory scope - and then, + // here, we switch the factory to bind scopes to requests + _factory.EnablePerWebRequestScope(); } protected virtual void ConfigureUnhandledException() From acad9d1f840f8e5a92cf24517df94b28d316fa1a Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 11:03:04 +0200 Subject: [PATCH 69/74] Making class public --- .../Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs index e2046209d5..269e437d0d 100644 --- a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Common.Filters /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains /// a partial view macro form. /// - internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + public class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute { /// From 062441c650c2f42c2d36e1e908f0474bc28bf72b Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 21 Apr 2020 13:48:06 +0200 Subject: [PATCH 70/74] Passing NoAppCache to CoreRuntime base ctor --- src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index 4763d3dbc6..960d355c0d 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -50,7 +50,7 @@ namespace Umbraco.Tests.Routing public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache()) + : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance) { } From 46e14f769227453bc1ca806ed0dec2bb9157a2db Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 22 Apr 2020 14:23:56 +1000 Subject: [PATCH 71/74] Fixes up all logging. Configures serilog logging to replace MS logging just like serilog does, adds new ILoggerConfiguration so we aren't hard coding things, dynamically adds enrichers when the container is ready to resolve services. --- .../Logging/ILoggingConfiguration.cs | 13 ++ .../Logging/LoggingConfiguration.cs | 20 ++++ .../Logging/MessageTemplates.cs | 8 +- .../Enrichers/HttpRequestIdEnricher.cs | 13 +- .../Enrichers/HttpRequestNumberEnricher.cs | 13 +- .../Enrichers/HttpSessionIdEnricher.cs | 2 +- .../Logging/Serilog/LoggerConfigExtensions.cs | 46 +++---- .../Logging/Serilog/SerilogLogger.cs | 113 +++--------------- .../Serilog/ThreadAbortExceptionEnricher.cs | 98 +++++++++++++++ .../Logging/Viewer/ILogViewer.cs | 4 +- .../Logging/Viewer/LogViewerComposer.cs | 2 +- ...onLogViewer.cs => SerilogJsonLogViewer.cs} | 21 ++-- ...eBase.cs => SerilogLogViewerSourceBase.cs} | 8 +- src/Umbraco.Tests.Common/TestHelperBase.cs | 9 ++ .../Implementations/TestHostingEnvironment.cs | 1 + src/Umbraco.Tests.Integration/RuntimeTests.cs | 4 +- .../Testing/UmbracoIntegrationTest.cs | 2 +- src/Umbraco.Tests/Logging/LogviewerTests.cs | 8 +- src/Umbraco.Tests/TestHelpers/TestHelper.cs | 2 + src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 +- .../UmbracoExamine/ExamineBaseTest.cs | 2 +- .../UmbracoApplicationBuilderExtensions.cs | 11 ++ .../AspNetCoreHostingEnvironment.cs | 4 +- .../Extensions/HostBuilderExtensions.cs | 4 +- .../UmbracoCoreServiceCollectionExtensions.cs | 71 +++++++++-- .../UmbracoRequestLoggingMiddleware.cs | 36 ++++++ .../Middleware/UmbracoRequestMiddleware.cs | 1 - .../Config/serilog.Release.config | 4 +- .../Config/serilog.config | 4 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 2 + .../config/serilog.Release.config | 4 +- src/Umbraco.Web/UmbracoApplicationBase.cs | 18 ++- 32 files changed, 359 insertions(+), 191 deletions(-) create mode 100644 src/Umbraco.Core/Logging/ILoggingConfiguration.cs create mode 100644 src/Umbraco.Core/Logging/LoggingConfiguration.cs create mode 100644 src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs rename src/Umbraco.Infrastructure/Logging/Viewer/{JsonLogViewer.cs => SerilogJsonLogViewer.cs} (79%) rename src/Umbraco.Infrastructure/Logging/Viewer/{LogViewerSourceBase.cs => SerilogLogViewerSourceBase.cs} (93%) create mode 100644 src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs diff --git a/src/Umbraco.Core/Logging/ILoggingConfiguration.cs b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs new file mode 100644 index 0000000000..47e2d8fa7c --- /dev/null +++ b/src/Umbraco.Core/Logging/ILoggingConfiguration.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Logging +{ + + public interface ILoggingConfiguration + { + /// + /// The physical path where logs are stored + /// + string LogDirectory { get; } + string LogConfigurationFile { get; } + string UserLogConfigurationFile { get; } + } +} diff --git a/src/Umbraco.Core/Logging/LoggingConfiguration.cs b/src/Umbraco.Core/Logging/LoggingConfiguration.cs new file mode 100644 index 0000000000..c657c9d430 --- /dev/null +++ b/src/Umbraco.Core/Logging/LoggingConfiguration.cs @@ -0,0 +1,20 @@ +using System; + +namespace Umbraco.Core.Logging +{ + public class LoggingConfiguration : ILoggingConfiguration + { + public LoggingConfiguration(string logDirectory, string logConfigurationFile, string userLogConfigurationFile) + { + LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory)); + LogConfigurationFile = logConfigurationFile ?? throw new ArgumentNullException(nameof(logConfigurationFile)); + UserLogConfigurationFile = userLogConfigurationFile ?? throw new ArgumentNullException(nameof(userLogConfigurationFile)); + } + + public string LogDirectory { get; } + + public string LogConfigurationFile { get; } + + public string UserLogConfigurationFile { get; } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs index 4640007e1a..712ff85e16 100644 --- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs +++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs @@ -16,14 +16,14 @@ namespace Umbraco.Core.Logging // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. + // TODO: Do we still need this, is there a non-pre release package shipped? + private static readonly Lazy MinimalLogger = new Lazy(() => new LoggerConfiguration().CreateLogger()); public string Render(string messageTemplate, params object[] args) { - // by default, unless initialized otherwise, Log.Logger is SilentLogger which cannot bind message - // templates. Log.Logger is set to a true Logger when initializing Umbraco's logger, but in case - // that has not been done already - use a temp minimal logger (eg for tests). - var logger = Log.Logger as global::Serilog.Core.Logger ?? MinimalLogger.Value; + // resolve a minimal logger instance which is used to bind message templates + var logger = MinimalLogger.Value; var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs index 45468ace9f..704e80d302 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs @@ -11,13 +11,13 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs /// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want /// - internal class HttpRequestIdEnricher : ILogEventEnricher + public class HttpRequestIdEnricher : ILogEventEnricher { - private readonly Func _requestCacheGetter; + private readonly IRequestCache _requestCache; - public HttpRequestIdEnricher(Func requestCacheGetter) + public HttpRequestIdEnricher(IRequestCache requestCache) { - _requestCacheGetter = requestCacheGetter; + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); } /// @@ -34,11 +34,8 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - var requestCache = _requestCacheGetter(); - if(requestCache is null) return; - Guid requestId; - if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, requestCache)) + if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache)) return; var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs index 08eb6b93f0..20643ff539 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs @@ -13,9 +13,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs /// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want /// - internal class HttpRequestNumberEnricher : ILogEventEnricher + public class HttpRequestNumberEnricher : ILogEventEnricher { - private readonly Func _requestCacheGetter; + private readonly IRequestCache _requestCache; private static int _lastRequestNumber; private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; @@ -25,9 +25,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers private const string _httpRequestNumberPropertyName = "HttpRequestNumber"; - public HttpRequestNumberEnricher(Func requestCacheGetter) + public HttpRequestNumberEnricher(IRequestCache requestCache) { - _requestCacheGetter = requestCacheGetter; + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); } /// @@ -39,10 +39,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - var requestCache = _requestCacheGetter(); - if (requestCache is null) return; - - var requestNumber = requestCache.Get(_requestNumberItemName, + var requestNumber = _requestCache.Get(_requestNumberItemName, () => Interlocked.Increment(ref _lastRequestNumber)); var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber)); diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs index 1558cdcf21..19572b5b42 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs /// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want /// - internal class HttpSessionIdEnricher : ILogEventEnricher + public class HttpSessionIdEnricher : ILogEventEnricher { private readonly ISessionIdResolver _sessionIdResolver; diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index b0521c6da8..dfcc401ea3 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -7,11 +7,8 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Compact; -using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Hosting; using Umbraco.Core.Logging.Serilog.Enrichers; -using Umbraco.Net; namespace Umbraco.Core.Logging.Serilog { @@ -25,27 +22,30 @@ namespace Umbraco.Core.Logging.Serilog /// It is highly recommended that you keep/use this default in your own logging config customizations /// /// A Serilog LoggerConfiguration - /// - public static LoggerConfiguration MinimalConfiguration(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func requestCacheGetter) + /// + /// + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration) { global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> + Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.ApplicationPhysicalPath, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) .Enrich.WithProcessId() .Enrich.WithProcessName() .Enrich.WithThreadId() - .Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id) + .Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id) .Enrich.WithProperty("AppDomainAppId", hostingEnvironment.ApplicationId.ReplaceNonAlphanumericChars(string.Empty)) .Enrich.WithProperty("MachineName", Environment.MachineName) .Enrich.With() - .Enrich.With(new HttpSessionIdEnricher(sessionIdResolver)) - .Enrich.With(new HttpRequestNumberEnricher(requestCacheGetter)) - .Enrich.With(new HttpRequestIdEnricher(requestCacheGetter)); + .Enrich.FromLogContext(); // allows us to dynamically enrich return logConfig; } @@ -54,13 +54,14 @@ namespace Umbraco.Core.Logging.Serilog /// Outputs a .txt format log at /App_Data/Logs/ /// /// A Serilog LoggerConfiguration + /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept - public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) + public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) { //Main .txt logfile - in similar format to older Log4Net output //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(Path.Combine(hostingEnvironment.ApplicationPhysicalPath, $@"App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt"), + logConfig.WriteTo.File(Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..txt"), shared: true, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: minimumLevel, @@ -86,8 +87,6 @@ namespace Umbraco.Core.Logging.Serilog Encoding encoding = null ) { - // TODO: Deal with this method call since it's obsolete, we need to change this - return configuration.Async( asyncConfiguration => asyncConfiguration.Map(AppDomainId, (_,mapConfiguration) => mapConfiguration.File( @@ -102,7 +101,8 @@ namespace Umbraco.Core.Logging.Serilog rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, - encoding), + encoding, + null), sinkMapCountLimit:0) ); } @@ -112,13 +112,14 @@ namespace Umbraco.Core.Logging.Serilog /// Outputs a CLEF format JSON log at /App_Data/Logs/ /// /// A Serilog LoggerConfiguration + /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept - public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) + public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) { //.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(new CompactJsonFormatter(), Path.Combine(hostingEnvironment.ApplicationPhysicalPath, $@"App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json"), + logConfig.WriteTo.File(new CompactJsonFormatter(), Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..json"), shared: true, rollingInterval: RollingInterval.Day, //Create a new JSON file every day retainedFileCountLimit: retainedFileCount, //Setting to null means we keep all files - default is 31 days @@ -132,10 +133,11 @@ namespace Umbraco.Core.Logging.Serilog /// That allows the main logging pipeline to be configured /// /// A Serilog LoggerConfiguration - public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment) + /// + public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration) { //Read from main serilog.config file - logConfig.ReadFrom.AppSettings(filePath: Path.Combine(hostingEnvironment.ApplicationPhysicalPath, @"config\serilog.config")); + logConfig.ReadFrom.AppSettings(filePath: loggingConfiguration.LogConfigurationFile); return logConfig; } @@ -145,13 +147,15 @@ namespace Umbraco.Core.Logging.Serilog /// That allows a separate logging pipeline to be configured that will not affect the main Umbraco log /// /// A Serilog LoggerConfiguration - public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment) + /// + public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration) { //A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above logConfig.WriteTo.Logger(cfg => - cfg.ReadFrom.AppSettings(filePath: Path.Combine(hostingEnvironment.ApplicationPhysicalPath, @"config\serilog.user.config"))); + cfg.ReadFrom.AppSettings(filePath: loggingConfiguration.UserLogConfigurationFile)); return logConfig; } + } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index e4695dedd1..38af9554ab 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -1,72 +1,56 @@ using System; using System.IO; -using System.Reflection; -using System.Threading; using Serilog; using Serilog.Events; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Diagnostics; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Net; namespace Umbraco.Core.Logging.Serilog { + /// /// Implements on top of Serilog. /// public class SerilogLogger : ILogger, IDisposable { - private readonly ICoreDebugSettings _coreDebugSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMarchal _marchal; + public global::Serilog.ILogger SerilogLog { get; } /// /// Initialize a new instance of the class with a configuration file. /// /// - public SerilogLogger(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal, FileInfo logConfigFile) + public SerilogLogger(FileInfo logConfigFile) { - _coreDebugSettings = coreDebugSettings; - _hostingEnvironment = hostingEnvironment; - _marchal = marchal; - - Log.Logger = new LoggerConfiguration() + SerilogLog = new LoggerConfiguration() .ReadFrom.AppSettings(filePath: logConfigFile.FullName) .CreateLogger(); } - public SerilogLogger(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal, LoggerConfiguration logConfig) + public SerilogLogger(LoggerConfiguration logConfig) { - _coreDebugSettings = coreDebugSettings; - _hostingEnvironment = hostingEnvironment; - _marchal = marchal; - //Configure Serilog static global logger with config passed in - Log.Logger = logConfig.CreateLogger(); + SerilogLog = logConfig.CreateLogger(); } /// /// Creates a logger with some pre-defined configuration and remainder from config file /// /// Used by UmbracoApplicationBase to get its logger. - public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func requestCacheGetter, ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal) + public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration) { var loggerConfig = new LoggerConfiguration(); loggerConfig - .MinimalConfiguration(hostingEnvironment, sessionIdResolver, requestCacheGetter) - .ReadFromConfigFile(hostingEnvironment) - .ReadFromUserConfigFile(hostingEnvironment); + .MinimalConfiguration(hostingEnvironment, loggingConfiguration) + .ReadFromConfigFile(loggingConfiguration) + .ReadFromUserConfigFile(loggingConfiguration); - return new SerilogLogger(coreDebugSettings, hostingEnvironment, marchal, loggerConfig); + return new SerilogLogger(loggerConfig); } /// /// Gets a contextualized logger. /// private global::Serilog.ILogger LoggerFor(Type reporting) - => Log.Logger.ForContext(reporting); + => SerilogLog.ForContext(reporting); /// /// Maps Umbraco's log level to Serilog's. @@ -99,8 +83,7 @@ namespace Umbraco.Core.Logging.Serilog /// public void Fatal(Type reporting, Exception exception, string message) { - var logger = LoggerFor(reporting); - DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message); + var logger = LoggerFor(reporting); logger.Fatal(exception, message); } @@ -108,8 +91,7 @@ namespace Umbraco.Core.Logging.Serilog public void Fatal(Type reporting, Exception exception) { var logger = LoggerFor(reporting); - var message = "Exception."; - DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message); + var message = "Exception."; logger.Fatal(exception, message); } @@ -128,16 +110,14 @@ namespace Umbraco.Core.Logging.Serilog /// public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) { - var logger = LoggerFor(reporting); - DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate); + var logger = LoggerFor(reporting); logger.Fatal(exception, messageTemplate, propertyValues); } /// public void Error(Type reporting, Exception exception, string message) { - var logger = LoggerFor(reporting); - DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message); + var logger = LoggerFor(reporting); logger.Error(exception, message); } @@ -146,7 +126,6 @@ namespace Umbraco.Core.Logging.Serilog { var logger = LoggerFor(reporting); var message = "Exception"; - DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message); logger.Error(exception, message); } @@ -166,67 +145,9 @@ namespace Umbraco.Core.Logging.Serilog public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) { var logger = LoggerFor(reporting); - DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate); logger.Error(exception, messageTemplate, propertyValues); } - private void DumpThreadAborts(global::Serilog.ILogger logger, LogEventLevel level, Exception exception, ref string messageTemplate) - { - var dump = false; - - if (IsTimeoutThreadAbortException(exception)) - { - messageTemplate += "\r\nThe thread has been aborted, because the request has timed out."; - - // dump if configured, or if stacktrace contains Monitor.ReliableEnter - dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception); - - // dump if it is ok to dump (might have a cap on number of dump...) - dump &= MiniDump.OkToDump(_hostingEnvironment); - } - - if (dump) - { - try - { - var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); - messageTemplate += dumped - ? "\r\nA minidump was created in App_Data/MiniDump" - : "\r\nFailed to create a minidump"; - } - catch (Exception ex) - { - messageTemplate += "\r\nFailed to create a minidump"; - - //Log a new entry (as opposed to appending to same log entry) - logger.Write(level, ex, "Failed to create a minidump ({ExType}: {ExMessage})", - new object[]{ ex.GetType().FullName, ex.Message }); - } - } - } - - private static bool IsMonitorEnterThreadAbortException(Exception exception) - { - if (!(exception is ThreadAbortException abort)) return false; - - var stacktrace = abort.StackTrace; - return stacktrace.Contains("System.Threading.Monitor.ReliableEnter"); - } - - private static bool IsTimeoutThreadAbortException(Exception exception) - { - if (!(exception is ThreadAbortException abort)) return false; - if (abort.ExceptionState == null) return false; - - var stateType = abort.ExceptionState.GetType(); - if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false; - - var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); - if (timeoutField == null) return false; - - return (bool) timeoutField.GetValue(abort.ExceptionState); - } - /// public void Warn(Type reporting, string message) { @@ -289,7 +210,7 @@ namespace Umbraco.Core.Logging.Serilog public void Dispose() { - Log.CloseAndFlush(); + SerilogLog.DisposeIfDisposable(); } } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs new file mode 100644 index 0000000000..2a7d35b636 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs @@ -0,0 +1,98 @@ +using System; +using System.Reflection; +using System.Threading; +using Serilog.Core; +using Serilog.Events; +using Umbraco.Core.Configuration; +using Umbraco.Core.Diagnostics; +using Umbraco.Core.Hosting; + +namespace Umbraco.Core.Logging.Serilog +{ + /// + /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can + /// + public class ThreadAbortExceptionEnricher : ILogEventEnricher + { + private readonly ICoreDebugSettings _coreDebugSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IMarchal _marchal; + + public ThreadAbortExceptionEnricher(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal) + { + _coreDebugSettings = coreDebugSettings; + _hostingEnvironment = hostingEnvironment; + _marchal = marchal; + } + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + switch (logEvent.Level) + { + case LogEventLevel.Error: + case LogEventLevel.Fatal: + DumpThreadAborts(logEvent, propertyFactory); + break; + } + } + + private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!IsTimeoutThreadAbortException(logEvent.Exception)) return; + + var message = "The thread has been aborted, because the request has timed out."; + + // dump if configured, or if stacktrace contains Monitor.ReliableEnter + var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception); + + // dump if it is ok to dump (might have a cap on number of dump...) + dump &= MiniDump.OkToDump(_hostingEnvironment); + + if (!dump) + { + message += ". No minidump was created."; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); + } + else + { + try + { + var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); + message += dumped + ? ". A minidump was created in App_Data/MiniDump." + : ". Failed to create a minidump."; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); + } + catch (Exception ex) + { + message = "Failed to create a minidump. " + ex; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); + } + } + } + + private static bool IsTimeoutThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) return false; + if (abort.ExceptionState == null) return false; + + var stateType = abort.ExceptionState.GetType(); + if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false; + + var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); + if (timeoutField == null) return false; + + return (bool)timeoutField.GetValue(abort.ExceptionState); + } + + private static bool IsMonitorEnterThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) return false; + + var stacktrace = abort.StackTrace; + return stacktrace.Contains("System.Threading.Monitor.ReliableEnter"); + } + + + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index dbdd7842ba..6763b0ebbb 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Logging.Viewer { diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs index e4acde1265..ee115be325 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Logging.Viewer public void Compose(Composition composition) { composition.RegisterUnique(); - composition.SetLogViewer(); + composition.SetLogViewer(); } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs similarity index 79% rename from src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs rename to src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index 54dd58ec03..366a0fb9de 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/JsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -10,23 +10,20 @@ using Umbraco.Core.IO; namespace Umbraco.Core.Logging.Viewer { - internal class JsonLogViewer : LogViewerSourceBase + internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase { private readonly string _logsPath; private readonly ILogger _logger; - private readonly IHostingEnvironment _hostingEnvironment; - public JsonLogViewer(ILogger logger, ILogViewerConfig logViewerConfig, IHostingEnvironment hostingEnvironment) : base(logViewerConfig) + public SerilogJsonLogViewer( + ILogger logger, + ILogViewerConfig logViewerConfig, + ILoggingConfiguration loggingConfiguration, + global::Serilog.ILogger serilogLog) + : base(logViewerConfig, serilogLog) { - _hostingEnvironment = hostingEnvironment; _logger = logger; - - // TODO: this path is hard coded but it can actually be configured, but that is done via Serilog and we don't have a different abstraction/config - // for the logging path. We could make that, but then how would we get that abstraction into the Serilog config? I'm sure there is a way but - // don't have time right now to resolve that (since this was hard coded before). We could have a single/simple ILogConfig for umbraco that purely specifies - // the logging path and then we can have a special token that we replace in the serilog config that maps to that location? then at least we could inject - // that config in places where we are hard coding this path. - _logsPath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, @"App_Data\Logs\"); + _logsPath = loggingConfiguration.LogDirectory; } private const int FileSizeCap = 100; @@ -133,7 +130,7 @@ namespace Umbraco.Core.Logging.Viewer { // As we are reading/streaming one line at a time in the JSON file // Thus we can not report the line number, as it will always be 1 - _logger.Error(ex, "Unable to parse a line in the JSON log file"); + _logger.Error(ex, "Unable to parse a line in the JSON log file"); evt = null; return true; diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs similarity index 93% rename from src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs rename to src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index aae2976044..7c8503a37e 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -8,13 +8,15 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Logging.Viewer { - public abstract class LogViewerSourceBase : ILogViewer + public abstract class SerilogLogViewerSourceBase : ILogViewer { private readonly ILogViewerConfig _logViewerConfig; + private readonly global::Serilog.ILogger _serilogLog; - protected LogViewerSourceBase(ILogViewerConfig logViewerConfig) + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog) { _logViewerConfig = logViewerConfig; + _serilogLog = serilogLog; } public abstract bool CanHandleLargeLogs { get; } @@ -48,7 +50,7 @@ namespace Umbraco.Core.Logging.Viewer /// public string GetLogLevel() { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.Logger.IsEnabled)?.Min() ?? null; + var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(_serilogLog.IsEnabled)?.Min() ?? null; return logLevel?.ToString() ?? ""; } diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs index 42b1e6c0dd..21b1f66395 100644 --- a/src/Umbraco.Tests.Common/TestHelperBase.cs +++ b/src/Umbraco.Tests.Common/TestHelperBase.cs @@ -152,5 +152,14 @@ namespace Umbraco.Tests.Common return mock.Object; } + + public ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv = null) + { + hostingEnv = hostingEnv ?? GetHostingEnvironment(); + return new LoggingConfiguration( + Path.Combine(hostingEnv.ApplicationPhysicalPath, "App_Data\\Logs"), + Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.config"), + Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.user.config")); + } } } diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs index 6430291bc2..9f29b14858 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Web.Common.AspNetCore; namespace Umbraco.Tests.Integration.Implementations diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 52c29d2037..9bc8d31ded 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -99,7 +99,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); @@ -138,7 +138,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 101feb79a4..f3a2bcf011 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -108,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests/Logging/LogviewerTests.cs b/src/Umbraco.Tests/Logging/LogviewerTests.cs index 75ff81a6d5..0a193b4446 100644 --- a/src/Umbraco.Tests/Logging/LogviewerTests.cs +++ b/src/Umbraco.Tests/Logging/LogviewerTests.cs @@ -1,9 +1,11 @@ using Moq; using NUnit.Framework; +using Serilog; using System; using System.IO; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Logging.Viewer; using Umbraco.Tests.TestHelpers; @@ -35,8 +37,10 @@ namespace Umbraco.Tests.Logging var ioHelper = TestHelper.IOHelper; var hostingEnv = TestHelper.GetHostingEnvironment(); + var loggingConfiguration = TestHelper.GetLoggingConfiguration(hostingEnv); + var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName); - _newLogfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"App_Data\Logs\"); + _newLogfileDirPath = loggingConfiguration.LogDirectory; _newLogfilePath = Path.Combine(_newLogfileDirPath, _logfileName); var exampleSearchfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _searchfileName); @@ -53,7 +57,7 @@ namespace Umbraco.Tests.Logging var logger = Mock.Of(); var logViewerConfig = new LogViewerConfig(hostingEnv); - _logViewer = new JsonLogViewer(logger, logViewerConfig, hostingEnv); + _logViewer = new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, Log.Logger); } [OneTimeTearDown] diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 7eca49183d..6fcba1ba1a 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -324,6 +324,8 @@ namespace Umbraco.Tests.TestHelpers public static IHostingEnvironment GetHostingEnvironment() => _testHelperInternal.GetHostingEnvironment(); + public static ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv) => _testHelperInternal.GetLoggingConfiguration(hostingEnv); + public static IApplicationShutdownRegistry GetHostingEnvironmentLifetime() => _testHelperInternal.GetHostingEnvironmentLifetime(); public static IIpResolver GetIpResolver() => _testHelperInternal.GetIpResolver(); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index f62a69177a..80f6ab9c9e 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -264,7 +264,7 @@ namespace Umbraco.Tests.Testing profiler = Mock.Of(); break; case UmbracoTestOptions.Logger.Serilog: - logger = new SerilogLogger(TestHelper.CoreDebugSettings, HostingEnvironment, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); + logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); profiler = new LogProfiler(logger); break; case UmbracoTestOptions.Logger.Console: diff --git a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs index 76b989a7a3..0d55fd99d7 100644 --- a/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/ExamineBaseTest.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.UmbracoExamine public void InitializeFixture() { - var logger = new SerilogLogger(TestHelper.CoreDebugSettings, HostingEnvironment, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); + var logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config"))); _profilingLogger = new ProfilingLogger(logger, new LogProfiler(logger)); } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs index ddf06e6532..88b9b4ca9e 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs @@ -1,9 +1,11 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Serilog.Context; using Smidge; using Umbraco.Core; using Umbraco.Core.Hosting; +using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.BackOffice.AspNetCore { @@ -64,6 +66,15 @@ namespace Umbraco.Web.BackOffice.AspNetCore } } + public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app) + { + if (app == null) throw new ArgumentNullException(nameof(app)); + + app.UseMiddleware(); + + return app; + } + public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index decfcfa660..b3ec11c241 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -5,13 +5,12 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreHostingEnvironment : Umbraco.Core.Hosting.IHostingEnvironment { - - private readonly IHostingSettings _hostingSettings; private readonly IWebHostEnvironment _webHostEnvironment; @@ -28,6 +27,7 @@ namespace Umbraco.Web.Common.AspNetCore ApplicationVirtualPath = "/"; //TODO how to find this, This is a server thing, not application thing. IISVersion = new Version(0, 0); // TODO not necessary IIS + } public bool IsHosted { get; } = true; diff --git a/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs index 3cb0922837..d314a2ae30 100644 --- a/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Hosting; -using Serilog; using Umbraco.Core.Composing; namespace Umbraco.Web.Common.Extensions @@ -13,8 +12,7 @@ namespace Umbraco.Web.Common.Extensions /// public static IHostBuilder UseUmbraco(this IHostBuilder builder) { - return builder - .UseSerilog() + return builder .UseUmbraco(new UmbracoServiceProviderFactory()); } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 843620d571..80482852f3 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -6,6 +6,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Extensions.Hosting; +using Serilog.Extensions.Logging; using Smidge; using Smidge.Nuglify; using Umbraco.Composing; @@ -71,7 +75,12 @@ namespace Umbraco.Web.Common.Extensions var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer; - services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), out factory); + var loggingConfig = new LoggingConfiguration( + Path.Combine(webHostEnvironment.ContentRootPath, "App_Data\\Logs"), + Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.config"), + Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.user.config")); + + services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), loggingConfig, out factory); return services; } @@ -83,9 +92,16 @@ namespace Umbraco.Web.Common.Extensions /// /// /// + /// /// /// - public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly, out IFactory factory) + public static IServiceCollection AddUmbracoCore( + this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + IRegister umbContainer, + Assembly entryAssembly, + ILoggingConfiguration loggingConfiguration, + out IFactory factory) { if (services is null) throw new ArgumentNullException(nameof(services)); var container = umbContainer; @@ -96,7 +112,7 @@ namespace Umbraco.Web.Common.Extensions // we resolve it before the host finishes configuring in the call to CreateCompositionRoot services.AddSingleton(); - CreateCompositionRoot(services, webHostEnvironment, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); + CreateCompositionRoot(services, webHostEnvironment, loggingConfiguration, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); var globalSettings = configs.Global(); var umbracoVersion = new UmbracoVersion(globalSettings); @@ -116,7 +132,7 @@ namespace Umbraco.Web.Common.Extensions return services; } - private static ITypeFinder CreateTypeFinder(ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly) + private static ITypeFinder CreateTypeFinder(Core.Logging.ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly) { // TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however // this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now. @@ -126,7 +142,7 @@ namespace Umbraco.Web.Common.Extensions return new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly), runtimeHash); } - private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, + private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, ITypeFinder typeFinder) { @@ -151,8 +167,11 @@ namespace Umbraco.Web.Common.Extensions return coreRuntime; } - private static IServiceCollection CreateCompositionRoot(IServiceCollection services, IWebHostEnvironment webHostEnvironment, - out ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment, + private static IServiceCollection CreateCompositionRoot( + IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + ILoggingConfiguration loggingConfiguration, + out Core.Logging.ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment, out IBackOfficeInfo backOfficeInfo, out IProfiler profiler) { // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured @@ -170,11 +189,7 @@ namespace Umbraco.Web.Common.Extensions hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment); ioHelper = new IOHelper(hostingEnvironment, globalSettings); - logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, - new AspNetCoreSessionManager(httpContextAccessor), - // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured - () => services.BuildServiceProvider().GetService(), coreDebug, ioHelper, - new AspNetCoreMarchal()); + logger = AddLogger(services, hostingEnvironment, loggingConfiguration); backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings); profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor); @@ -182,6 +197,38 @@ namespace Umbraco.Web.Common.Extensions return services; } + /// + /// Create and configure the logger + /// + /// + private static Core.Logging.ILogger AddLogger(IServiceCollection services, Core.Hosting.IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration) + { + // Create a serilog logger + var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration); + + // Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since + // we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which + // is the only other option that these ext methods allow. + // I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that. + // Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs + + services.AddSingleton(services => new SerilogLoggerFactory(logger.SerilogLog, false)); + + // This won't (and shouldn't) take ownership of the logger. + services.AddSingleton(logger.SerilogLog); + + // Registered to provide two services... + var diagnosticContext = new DiagnosticContext(logger.SerilogLog); + + // Consumed by e.g. middleware + services.AddSingleton(diagnosticContext); + + // Consumed by user code + services.AddSingleton(diagnosticContext); + + return logger; + } + public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services, IConfiguration configuration) { diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs new file mode 100644 index 0000000000..f2034dbd82 --- /dev/null +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Serilog.Context; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging.Serilog.Enrichers; +using Umbraco.Net; + +namespace Umbraco.Web.Common.Middleware +{ + public class UmbracoRequestLoggingMiddleware + { + readonly RequestDelegate _next; + private readonly ISessionIdResolver _sessionIdResolver; + private readonly IRequestCache _requestCache; + + public UmbracoRequestLoggingMiddleware(RequestDelegate next, ISessionIdResolver sessionIdResolver, IRequestCache requestCache) + { + _next = next; + _sessionIdResolver = sessionIdResolver; + _requestCache = requestCache; + } + + public async Task Invoke(HttpContext httpContext) + { + // TODO: Need to decide if we want this stuff still, there's new request logging in serilog: + // https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this? + + using (LogContext.Push(new HttpSessionIdEnricher(_sessionIdResolver))) + using (LogContext.Push(new HttpRequestNumberEnricher(_requestCache))) + using (LogContext.Push(new HttpRequestIdEnricher(_requestCache))) + { + await _next(httpContext); + } + } + } +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 93461fc1d5..e8695f3c9c 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -22,5 +22,4 @@ namespace Umbraco.Web.Common.Middleware _umbracoRequestLifetimeManager.EndRequest(context); } } - } diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config index e3cf52b3c5..9aca408b36 100644 --- a/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.Release.config @@ -19,7 +19,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/src/Umbraco.Web.UI.NetCore/Config/serilog.config b/src/Umbraco.Web.UI.NetCore/Config/serilog.config index e3cf52b3c5..9aca408b36 100644 --- a/src/Umbraco.Web.UI.NetCore/Config/serilog.config +++ b/src/Umbraco.Web.UI.NetCore/Config/serilog.config @@ -19,7 +19,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 75b2d6f48e..d79fa0d917 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -54,6 +54,7 @@ namespace Umbraco.Web.UI.BackOffice }); //Finally initialize Current + // TODO: This should be moved to the UmbracoServiceProviderFactory when the container is cross-wired and then don't use the overload above to `out var factory` Current.Initialize( factory.GetInstance (), factory.GetInstance(), @@ -76,6 +77,7 @@ namespace Umbraco.Web.UI.BackOffice app.UseDeveloperExceptionPage(); } app.UseUmbracoCore(); + app.UseUmbracoRequestLogging(); app.UseUmbracoWebsite(); app.UseUmbracoBackOffice(); app.UseRouting(); diff --git a/src/Umbraco.Web.UI/config/serilog.Release.config b/src/Umbraco.Web.UI/config/serilog.Release.config index e3cf52b3c5..9aca408b36 100644 --- a/src/Umbraco.Web.UI/config/serilog.Release.config +++ b/src/Umbraco.Web.UI/config/serilog.Release.config @@ -19,7 +19,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index d884366bf1..1f90bc7d13 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -1,4 +1,5 @@ -using System; +using Serilog.Context; +using System; using System.IO; using System.Reflection; using System.Threading; @@ -12,6 +13,8 @@ using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; +using Umbraco.Core.Logging.Serilog.Enrichers; +using Umbraco.Net; using Umbraco.Web.AspNet; using Umbraco.Web.Hosting; using Umbraco.Web.Logging; @@ -34,12 +37,16 @@ namespace Umbraco.Web var configFactory = new ConfigsFactory(); var hostingSettings = configFactory.HostingSettings; - var coreDebug = configFactory.CoreDebugSettings; var globalSettings = configFactory.GlobalSettings; var hostingEnvironment = new AspNetHostingEnvironment(hostingSettings); + var loggingConfiguration = new LoggingConfiguration( + Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data\\Logs"), + Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "config\\serilog.config"), + Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "config\\serilog.user.config")); var ioHelper = new IOHelper(hostingEnvironment, globalSettings); - var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, new AspNetSessionManager(), () => _factory?.GetInstance(), coreDebug, ioHelper, new FrameworkMarchal()); + var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration); + var configs = configFactory.Create(); var backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, ioHelper, logger, configFactory.WebRoutingSettings); @@ -168,6 +175,11 @@ namespace Umbraco.Web Umbraco.Composing.Current.BackOfficeInfo); _factory = Current.Factory = _runtime.Configure(register); + // now we can add our request based logging enrichers (globally, which is what we were doing in netframework before) + LogContext.Push(new HttpSessionIdEnricher(_factory.GetInstance())); + LogContext.Push(new HttpRequestNumberEnricher(_factory.GetInstance())); + LogContext.Push(new HttpRequestIdEnricher(_factory.GetInstance())); + _runtime.Start(); } From 2ef8f26b2858cd7c1bde3bd8e7fc4b5aeb16d1f2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 22 Apr 2020 14:29:01 +1000 Subject: [PATCH 72/74] removes unneeded extra HostBuilderExtensions --- .../Composing/HostBuilderExtensions.cs | 11 +++++++++++ .../Extensions/HostBuilderExtensions.cs | 19 ------------------- src/Umbraco.Web.UI.NetCore/Program.cs | 1 - 3 files changed, 11 insertions(+), 20 deletions(-) delete mode 100644 src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs index 3ccc3b8a8d..ab758d42af 100644 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -7,6 +7,17 @@ namespace Umbraco.Core.Composing /// public static class HostBuilderExtensions { + /// + /// Assigns a custom service provider factory to use Umbraco's container + /// + /// + /// + public static IHostBuilder UseUmbraco(this IHostBuilder builder) + { + return builder + .UseUmbraco(new UmbracoServiceProviderFactory()); + } + /// /// Assigns a custom service provider factory to use Umbraco's container /// diff --git a/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs deleted file mode 100644 index d314a2ae30..0000000000 --- a/src/Umbraco.Web.Common/Extensions/HostBuilderExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Umbraco.Core.Composing; - -namespace Umbraco.Web.Common.Extensions -{ - public static class HostBuilderExtensions - { - /// - /// Assigns a custom service provider factory to use Umbraco's container - /// - /// - /// - public static IHostBuilder UseUmbraco(this IHostBuilder builder) - { - return builder - .UseUmbraco(new UmbracoServiceProviderFactory()); - } - } -} diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index 1fe66f6664..1151f16be8 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Umbraco.Core.Composing; -using Umbraco.Web.Common.Extensions; namespace Umbraco.Web.UI.BackOffice { From 18b285f088df1508fb7b9686e78d750b962e7c3a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 22 Apr 2020 15:23:51 +1000 Subject: [PATCH 73/74] Splits profiler so it doesn't have to take a dependency since we don't want to pre-resolve. Adds enrichers to the container. --- src/Umbraco.Core/Logging/IProfiler.cs | 8 +-- src/Umbraco.Core/Logging/IProfilerHtml.cs | 15 ++++++ src/Umbraco.Core/Logging/LogProfiler.cs | 6 --- src/Umbraco.Core/Logging/VoidProfiler.cs | 5 -- .../ThreadAbortExceptionEnricher.cs | 6 +-- .../Logging/Serilog/SerilogComposer.cs | 21 ++++++++ .../TestHelpers/Stubs/TestProfiler.cs | 5 -- .../UmbracoApplicationBuilderExtensions.cs | 6 +++ .../UmbracoCoreServiceCollectionExtensions.cs | 9 ++-- .../UmbracoRequestLoggingMiddleware.cs | 21 +++++--- .../Runtime/AspNetCoreComposer.cs | 7 +++ .../Runtime/Profiler/WebProfiler.cs | 44 +--------------- .../Runtime/Profiler/WebProfilerComposer.cs | 9 +++- .../Runtime/Profiler/WebProfilerHtml.cs | 51 +++++++++++++++++++ src/Umbraco.Web.UI.NetCore/Startup.cs | 3 +- src/Umbraco.Web/Composing/Current.cs | 2 + src/Umbraco.Web/HtmlHelperRenderExtensions.cs | 2 +- src/Umbraco.Web/Logging/WebProfiler.cs | 2 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 ++- 19 files changed, 140 insertions(+), 88 deletions(-) create mode 100644 src/Umbraco.Core/Logging/IProfilerHtml.cs rename src/Umbraco.Infrastructure/Logging/Serilog/{ => Enrichers}/ThreadAbortExceptionEnricher.cs (98%) create mode 100644 src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs diff --git a/src/Umbraco.Core/Logging/IProfiler.cs b/src/Umbraco.Core/Logging/IProfiler.cs index 1327651197..d855612c95 100644 --- a/src/Umbraco.Core/Logging/IProfiler.cs +++ b/src/Umbraco.Core/Logging/IProfiler.cs @@ -2,18 +2,12 @@ namespace Umbraco.Core.Logging { + /// /// Defines the profiling service. /// public interface IProfiler { - /// - /// Renders the profiling results. - /// - /// The profiling results. - /// Generally used for HTML rendering. - string Render(); - /// /// Gets an that will time the code between its creation and disposal. /// diff --git a/src/Umbraco.Core/Logging/IProfilerHtml.cs b/src/Umbraco.Core/Logging/IProfilerHtml.cs new file mode 100644 index 0000000000..4f9ee62e0b --- /dev/null +++ b/src/Umbraco.Core/Logging/IProfilerHtml.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Core.Logging +{ + /// + /// Used to render a profiler in a web page + /// + public interface IProfilerHtml + { + /// + /// Renders the profiling results. + /// + /// The profiling results. + /// Generally used for HTML rendering. + string Render(); + } +} diff --git a/src/Umbraco.Core/Logging/LogProfiler.cs b/src/Umbraco.Core/Logging/LogProfiler.cs index 294f92dad3..a1d2a2e61f 100644 --- a/src/Umbraco.Core/Logging/LogProfiler.cs +++ b/src/Umbraco.Core/Logging/LogProfiler.cs @@ -15,12 +15,6 @@ namespace Umbraco.Core.Logging _logger = logger; } - /// - public string Render() - { - return string.Empty; - } - /// public IDisposable Step(string name) { diff --git a/src/Umbraco.Core/Logging/VoidProfiler.cs b/src/Umbraco.Core/Logging/VoidProfiler.cs index 51bec521a3..d771fd7630 100644 --- a/src/Umbraco.Core/Logging/VoidProfiler.cs +++ b/src/Umbraco.Core/Logging/VoidProfiler.cs @@ -6,11 +6,6 @@ namespace Umbraco.Core.Logging { private readonly VoidDisposable _disposable = new VoidDisposable(); - public string Render() - { - return string.Empty; - } - public IDisposable Step(string name) { return _disposable; diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs similarity index 98% rename from src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs rename to src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs index 2a7d35b636..1f495d3a50 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/ThreadAbortExceptionEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Diagnostics; using Umbraco.Core.Hosting; -namespace Umbraco.Core.Logging.Serilog +namespace Umbraco.Infrastructure.Logging.Serilog.Enrichers { /// /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can @@ -54,7 +54,6 @@ namespace Umbraco.Core.Logging.Serilog logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } else - { try { var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); @@ -68,7 +67,6 @@ namespace Umbraco.Core.Logging.Serilog message = "Failed to create a minidump. " + ex; logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } - } } private static bool IsTimeoutThreadAbortException(Exception exception) @@ -93,6 +91,6 @@ namespace Umbraco.Core.Logging.Serilog return stacktrace.Contains("System.Threading.Monitor.ReliableEnter"); } - + } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs new file mode 100644 index 0000000000..18b417d428 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogComposer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging.Serilog.Enrichers; +using Umbraco.Infrastructure.Logging.Serilog.Enrichers; + +namespace Umbraco.Infrastructure.Logging.Serilog +{ + public class SerilogComposer : ICoreComposer + { + public void Compose(Composition composition) + { + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + } + } +} diff --git a/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs b/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs index 39cac6e24f..ea0f9cc44f 100644 --- a/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs +++ b/src/Umbraco.Tests/TestHelpers/Stubs/TestProfiler.cs @@ -19,11 +19,6 @@ namespace Umbraco.Tests.TestHelpers.Stubs private static bool _enabled; - public string Render() - { - return string.Empty; - } - public IDisposable Step(string name) { return _enabled ? MiniProfiler.Current.Step(name) : null; diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs index 88b9b4ca9e..a27113e881 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoApplicationBuilderExtensions.cs @@ -4,7 +4,9 @@ using Microsoft.Extensions.DependencyInjection; using Serilog.Context; using Smidge; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; +using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.BackOffice.AspNetCore @@ -33,6 +35,10 @@ namespace Umbraco.Web.BackOffice.AspNetCore var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime); hostLifetime.RegisterObject(runtimeShutdown); + // Register our global threadabort enricher for logging + var threadAbortEnricher = app.ApplicationServices.GetRequiredService(); + LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context + // Start the runtime! runtime.Start(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 80482852f3..2439243f30 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -177,14 +177,11 @@ namespace Umbraco.Web.Common.Extensions // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured var serviceProvider = services.BuildServiceProvider(); - var httpContextAccessor = serviceProvider.GetRequiredService(); - configs = serviceProvider.GetService(); if (configs == null) throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}"); var hostingSettings = configs.Hosting(); - var coreDebug = configs.CoreDebug(); var globalSettings = configs.Global(); hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment); @@ -192,7 +189,7 @@ namespace Umbraco.Web.Common.Extensions logger = AddLogger(services, hostingEnvironment, loggingConfiguration); backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings); - profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor); + profiler = GetWebProfiler(hostingEnvironment); return services; } @@ -238,7 +235,7 @@ namespace Umbraco.Web.Common.Extensions return services; } - private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccessor) + private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment) { // create and start asap to profile boot if (!hostingEnvironment.IsDebugMode) @@ -248,7 +245,7 @@ namespace Umbraco.Web.Common.Extensions return new VoidProfiler(); } - var webProfiler = new WebProfiler(httpContextAccessor); + var webProfiler = new WebProfiler(); webProfiler.StartBoot(); return webProfiler; diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs index f2034dbd82..0e2158c939 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs @@ -10,14 +10,19 @@ namespace Umbraco.Web.Common.Middleware public class UmbracoRequestLoggingMiddleware { readonly RequestDelegate _next; - private readonly ISessionIdResolver _sessionIdResolver; - private readonly IRequestCache _requestCache; + private readonly HttpSessionIdEnricher _sessionIdEnricher; + private readonly HttpRequestNumberEnricher _requestNumberEnricher; + private readonly HttpRequestIdEnricher _requestIdEnricher; - public UmbracoRequestLoggingMiddleware(RequestDelegate next, ISessionIdResolver sessionIdResolver, IRequestCache requestCache) + public UmbracoRequestLoggingMiddleware(RequestDelegate next, + HttpSessionIdEnricher sessionIdEnricher, + HttpRequestNumberEnricher requestNumberEnricher, + HttpRequestIdEnricher requestIdEnricher) { _next = next; - _sessionIdResolver = sessionIdResolver; - _requestCache = requestCache; + _sessionIdEnricher = sessionIdEnricher; + _requestNumberEnricher = requestNumberEnricher; + _requestIdEnricher = requestIdEnricher; } public async Task Invoke(HttpContext httpContext) @@ -25,9 +30,9 @@ namespace Umbraco.Web.Common.Middleware // TODO: Need to decide if we want this stuff still, there's new request logging in serilog: // https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this? - using (LogContext.Push(new HttpSessionIdEnricher(_sessionIdResolver))) - using (LogContext.Push(new HttpRequestNumberEnricher(_requestCache))) - using (LogContext.Push(new HttpRequestIdEnricher(_requestCache))) + using (LogContext.Push(_sessionIdEnricher)) + using (LogContext.Push(_requestNumberEnricher)) + using (LogContext.Push(_requestIdEnricher)) { await _next(httpContext); } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 79c7d3ec25..8af2824c03 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -7,6 +7,9 @@ using Umbraco.Core.Runtime; using Umbraco.Core.Security; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Lifetime; +using Umbraco.Core.Diagnostics; +using Umbraco.Web.Common.Runtime.Profiler; +using Umbraco.Core.Logging; namespace Umbraco.Web.Common.Runtime { @@ -42,6 +45,10 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); composition.RegisterMultipleUnique(); + + composition.RegisterUnique(); + + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index bdbc6f164d..958e134bab 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -1,61 +1,21 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using StackExchange.Profiling; -using StackExchange.Profiling.Internal; using Umbraco.Core; using Umbraco.Core.Logging; +// TODO: This namespace is strange, not sure why i has "Runtime" in the name? namespace Umbraco.Web.Common.Runtime.Profiler { + public class WebProfiler : IProfiler { private MiniProfiler _startupProfiler; - - private readonly IHttpContextAccessor _httpContextAccessor; private int _first; - public WebProfiler(IHttpContextAccessor httpContextAccessor) - { - // create our own provider, which can provide a profiler even during boot - _httpContextAccessor = httpContextAccessor; - } - - /// - /// - /// - /// - /// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work. - /// We fake the requestIds from the RequestState here. - /// - public string Render() - { - - var profiler = MiniProfiler.Current; - if (profiler == null) return string.Empty; - - var context = _httpContextAccessor.HttpContext; - - var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash(); - - var result = StackExchange.Profiling.Internal.Render.Includes( - profiler, - path: context.Request.PathBase + path, - isAuthorized: true, - requestIDs: new List{ profiler.Id }, - position: RenderPosition.Right, - showTrivial: profiler.Options.PopupShowTrivial, - showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren, - maxTracesToShow: profiler.Options.PopupMaxTracesToShow, - showControls:profiler.Options.ShowControls, - startHidden: profiler.Options.PopupStartHidden); - - return result; - } - public IDisposable Step(string name) { return MiniProfiler.Current?.Step(name); diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs index 688a3e5c28..523faf2da5 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs @@ -1,8 +1,15 @@ -using Umbraco.Core.Composing; +using Umbraco.Core; +using Umbraco.Core.Composing; namespace Umbraco.Web.Common.Runtime.Profiler { internal class WebProfilerComposer : ComponentComposer, ICoreComposer { + public override void Compose(Composition composition) + { + base.Compose(composition); + + composition.RegisterUnique(); + } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs new file mode 100644 index 0000000000..9e989d6b5c --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using StackExchange.Profiling; +using StackExchange.Profiling.Internal; +using Umbraco.Core.Logging; + +// TODO: This namespace is strange, not sure why i has "Runtime" in the name? +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfilerHtml : IProfilerHtml + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public WebProfilerHtml(IHttpContextAccessor httpContextAccessor) + { + // create our own provider, which can provide a profiler even during boot + _httpContextAccessor = httpContextAccessor; + } + + /// + /// + /// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work. + /// We fake the requestIds from the RequestState here. + /// + public string Render() + { + + var profiler = MiniProfiler.Current; + if (profiler == null) return string.Empty; + + var context = _httpContextAccessor.HttpContext; + + var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash(); + + var result = StackExchange.Profiling.Internal.Render.Includes( + profiler, + path: context.Request.PathBase + path, + isAuthorized: true, + requestIDs: new List { profiler.Id }, + position: RenderPosition.Right, + showTrivial: profiler.Options.PopupShowTrivial, + showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren, + maxTracesToShow: profiler.Options.PopupMaxTracesToShow, + showControls: profiler.Options.ShowControls, + startHidden: profiler.Options.PopupStartHidden); + + return result; + } + } +} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d79fa0d917..8602d5bed6 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -91,7 +91,8 @@ namespace Umbraco.Web.UI.BackOffice }); endpoints.MapGet("/", async context => { - await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); + var profilerHtml = app.ApplicationServices.GetRequiredService(); + await context.Response.WriteAsync($"Hello World!{profilerHtml.Render()}"); }); }); } diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index f9f056ff32..1ed217cc78 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -248,6 +248,8 @@ namespace Umbraco.Web.Composing public static IProfiler Profiler => Factory.GetInstance(); + public static IProfilerHtml ProfilerHtml => Factory.GetInstance(); + public static IProfilingLogger ProfilingLogger => Factory.GetInstance(); public static AppCaches AppCaches => Factory.GetInstance(); diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 6c207dd15a..2795fe66c6 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web /// public static IHtmlString RenderProfiler(this HtmlHelper helper) { - return new HtmlString(Current.Profiler.Render()); + return new HtmlString(Current.ProfilerHtml.Render()); } /// diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index e390950c0b..6f15be7ecd 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Logging /// /// Profiling only runs when the app is in debug mode, see WebRuntime for how this gets created /// - internal class WebProfiler : IProfiler + internal class WebProfiler : IProfiler, IProfilerHtml { private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; private readonly WebProfilerProvider _provider; diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index ec3b463d4c..5b59b632eb 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -25,6 +25,9 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Umbraco.Net; using Umbraco.Web.AspNet; +using Umbraco.Core.Diagnostics; +using Umbraco.Core.Logging; +using Umbraco.Web.Logging; namespace Umbraco.Web.Runtime { @@ -50,7 +53,8 @@ namespace Umbraco.Web.Runtime composition.Register(Lifetime.Singleton); - + composition.RegisterUnique(); + composition.RegisterUnique(); composition.ComposeWebMappingProfiles(); From fce1df08f99978b2c4085ad59c2ce632f1e9e944 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 22 Apr 2020 10:55:13 +0200 Subject: [PATCH 74/74] Fix test --- src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index 0b90457b1e..f63c56b64e 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; using Umbraco.Core.Scoping; +using Umbraco.Net; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web; @@ -135,6 +136,7 @@ namespace Umbraco.Tests.Runtimes public override IFactory Configure(IRegister container) { container.Register(Lifetime.Singleton); + container.Register(Lifetime.Singleton); var factory = base.Configure(container); return factory;